mirror of
https://github.com/opelly27/streetmerchant.git
synced 2026-05-20 02:57:34 +00:00
feat(api): add rudimentary web control panel (#183)
This commit is contained in:
@@ -70,3 +70,4 @@ TWITTER_CONSUMER_KEY=
|
|||||||
TWITTER_CONSUMER_SECRET=
|
TWITTER_CONSUMER_SECRET=
|
||||||
TWITTER_TWEET_TAGS=
|
TWITTER_TWEET_TAGS=
|
||||||
USER_AGENT=
|
USER_AGENT=
|
||||||
|
WEB_PORT=
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ Here is a list of variables that you can use to customize your newly copied `.en
|
|||||||
| `TWITCH_REFRESH_TOKEN` | Twitch refresh token | |
|
| `TWITCH_REFRESH_TOKEN` | Twitch refresh token | |
|
||||||
| `TWITCH_CHANNEL` | Twitch channel | |
|
| `TWITCH_CHANNEL` | Twitch channel | |
|
||||||
| `USER_AGENT` | Custom User-Agents headers for HTTP requests | Newline separated, e.g.: `USER_AGENT_STRING1 \n USER_AGENT_STRING2` | | Default: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36` |
|
| `USER_AGENT` | Custom User-Agents headers for HTTP requests | Newline separated, e.g.: `USER_AGENT_STRING1 \n USER_AGENT_STRING2` | | Default: `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36` |
|
||||||
|
| `WEB_PORT` | Starts a webserver to be able to control the bot while it is running; optional | Default: disabled |
|
||||||
|
|
||||||
> :point_right: If you have multi-factor authentication (MFA), you will need to create an [app password](https://myaccount.google.com/apppasswords) and use this instead of your Gmail password.
|
> :point_right: If you have multi-factor authentication (MFA), you will need to create an [app password](https://myaccount.google.com/apppasswords) and use this instead of your Gmail password.
|
||||||
|
|
||||||
|
|||||||
@@ -271,3 +271,10 @@ export const config = {
|
|||||||
proxy,
|
proxy,
|
||||||
store
|
store
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function setConfig(newConfig: any) {
|
||||||
|
const writeConfig = config as any;
|
||||||
|
for (const key of Object.keys(newConfig)) {
|
||||||
|
writeConfig[key] = newConfig[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+39
-11
@@ -1,4 +1,5 @@
|
|||||||
import {Stores} from './store/model';
|
import {startAPIServer, stopAPIServer} from './web';
|
||||||
|
import {Browser} from 'puppeteer';
|
||||||
import {adBlocker} from './adblocker';
|
import {adBlocker} from './adblocker';
|
||||||
import {config} from './config';
|
import {config} from './config';
|
||||||
import {getSleepTime} from './util';
|
import {getSleepTime} from './util';
|
||||||
@@ -6,6 +7,7 @@ import {logger} from './logger';
|
|||||||
import puppeteer from 'puppeteer-extra';
|
import puppeteer from 'puppeteer-extra';
|
||||||
import resourceBlock from 'puppeteer-extra-plugin-block-resources';
|
import resourceBlock from 'puppeteer-extra-plugin-block-resources';
|
||||||
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
|
import stealthPlugin from 'puppeteer-extra-plugin-stealth';
|
||||||
|
import {storeList} from './store/model';
|
||||||
import {tryLookupAndLoop} from './store';
|
import {tryLookupAndLoop} from './store';
|
||||||
|
|
||||||
puppeteer.use(stealthPlugin());
|
puppeteer.use(stealthPlugin());
|
||||||
@@ -17,15 +19,12 @@ if (config.browser.lowBandwidth) {
|
|||||||
puppeteer.use(adBlocker);
|
puppeteer.use(adBlocker);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let browser: Browser | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts the bot.
|
* Starts the bot.
|
||||||
*/
|
*/
|
||||||
async function main() {
|
async function main() {
|
||||||
if (Stores.length === 0) {
|
|
||||||
logger.error('✖ no stores selected', Stores);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
// Skip Chromium Linux Sandbox
|
// Skip Chromium Linux Sandbox
|
||||||
@@ -45,7 +44,9 @@ async function main() {
|
|||||||
args.push(`--proxy-server=http://${config.proxy.address}:${config.proxy.port}`);
|
args.push(`--proxy-server=http://${config.proxy.address}:${config.proxy.port}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const browser = await puppeteer.launch({
|
await stop();
|
||||||
|
|
||||||
|
browser = await puppeteer.launch({
|
||||||
args,
|
args,
|
||||||
defaultViewport: {
|
defaultViewport: {
|
||||||
height: config.page.height,
|
height: config.page.height,
|
||||||
@@ -54,7 +55,7 @@ async function main() {
|
|||||||
headless: config.browser.isHeadless
|
headless: config.browser.isHeadless
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const store of Stores) {
|
for (const store of storeList.values()) {
|
||||||
logger.debug('store links', {meta: {links: store.links}});
|
logger.debug('store links', {meta: {links: store.links}});
|
||||||
if (store.setupAction !== undefined) {
|
if (store.setupAction !== undefined) {
|
||||||
store.setupAction(browser);
|
store.setupAction(browser);
|
||||||
@@ -62,14 +63,41 @@ async function main() {
|
|||||||
|
|
||||||
setTimeout(tryLookupAndLoop, getSleepTime(store), browser, store);
|
setTimeout(tryLookupAndLoop, getSleepTime(store), browser, store);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await startAPIServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stop() {
|
||||||
|
await stopAPIServer();
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
// Use temporary swap variable to avoid any race condition
|
||||||
|
const browserTemporary = browser;
|
||||||
|
browser = undefined;
|
||||||
|
await browserTemporary.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stopAndExit() {
|
||||||
|
await stop();
|
||||||
|
// eslint-disable-next-line unicorn/no-process-exit
|
||||||
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will continually run until user interferes.
|
* Will continually run until user interferes.
|
||||||
*/
|
*/
|
||||||
|
async function loopMain() {
|
||||||
try {
|
try {
|
||||||
void main();
|
await main();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('✖ something bad happened, resetting nvidia-snatcher', error);
|
logger.error('✖ something bad happened, resetting nvidia-snatcher in 5 seconds', error);
|
||||||
void main();
|
setTimeout(loopMain, 5000);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loopMain();
|
||||||
|
|
||||||
|
process.on('SIGINT', stopAndExit);
|
||||||
|
process.on('SIGQUIT', stopAndExit);
|
||||||
|
process.on('SIGTERM', stopAndExit);
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ const linkBuilderLastRunTimes: Record<string, number> = {};
|
|||||||
* @param store Vendor of graphics cards.
|
* @param store Vendor of graphics cards.
|
||||||
*/
|
*/
|
||||||
async function lookup(browser: Browser, store: Store) {
|
async function lookup(browser: Browser, store: Store) {
|
||||||
|
if (config.store.stores.length > 0 && !config.store.stores.find(foundStore => foundStore.name === store.name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
for (const link of store.links) {
|
for (const link of store.links) {
|
||||||
if (!filterStoreLink(link)) {
|
if (!filterStoreLink(link)) {
|
||||||
@@ -192,6 +196,11 @@ async function lookupCardInStock(store: Store, page: Page, link: Link) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function tryLookupAndLoop(browser: Browser, store: Store) {
|
export async function tryLookupAndLoop(browser: Browser, store: Store) {
|
||||||
|
if (!browser.isConnected()) {
|
||||||
|
logger.debug(`[${store.name}] Ending this loop as browser is disposed...`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (store.linksBuilder) {
|
if (store.linksBuilder) {
|
||||||
const lastRunTime = linkBuilderLastRunTimes[store.name] ?? -1;
|
const lastRunTime = linkBuilderLastRunTimes[store.name] ?? -1;
|
||||||
const ttl = store.linksBuilder.ttl ?? Number.MAX_SAFE_INTEGER;
|
const ttl = store.linksBuilder.ttl ?? Number.MAX_SAFE_INTEGER;
|
||||||
|
|||||||
+15
-24
@@ -42,13 +42,10 @@ import {ProshopDE} from './proshop-de';
|
|||||||
import {ProshopDK} from './proshop-dk';
|
import {ProshopDK} from './proshop-dk';
|
||||||
import {Saturn} from './saturn';
|
import {Saturn} from './saturn';
|
||||||
import {Scan} from './scan';
|
import {Scan} from './scan';
|
||||||
import {Store} from './store';
|
|
||||||
import {Very} from './very';
|
import {Very} from './very';
|
||||||
import {Zotac} from './zotac';
|
import {Zotac} from './zotac';
|
||||||
import {config} from '../../config';
|
|
||||||
import {logger} from '../../logger';
|
|
||||||
|
|
||||||
const masterList = new Map([
|
export const storeList = new Map([
|
||||||
[Adorama.name, Adorama],
|
[Adorama.name, Adorama],
|
||||||
[Alternate.name, Alternate],
|
[Alternate.name, Alternate],
|
||||||
[AlternateNL.name, AlternateNL],
|
[AlternateNL.name, AlternateNL],
|
||||||
@@ -97,33 +94,27 @@ const masterList = new Map([
|
|||||||
[Zotac.name, Zotac]
|
[Zotac.name, Zotac]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const list = new Map();
|
const brands = new Set();
|
||||||
|
const series = new Set();
|
||||||
for (const storeData of config.store.stores) {
|
const models = new Set();
|
||||||
if (masterList.has(storeData.name)) {
|
for (const store of storeList.values()) {
|
||||||
list.set(storeData.name, {...masterList.get(storeData.name), ...storeData});
|
for (const link of store.links) {
|
||||||
} else {
|
brands.add(link.brand);
|
||||||
const logString = `No store named ${storeData.name}, skipping.`;
|
series.add(link.series);
|
||||||
logger.warn(logString);
|
models.add(link.model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`ℹ selected stores: ${Array.from(list.keys()).join(', ')}`);
|
export function getAllBrands() {
|
||||||
|
return Array.from(brands);
|
||||||
if (config.store.showOnlyBrands.length > 0) {
|
|
||||||
logger.info(`ℹ selected brands: ${config.store.showOnlyBrands.join(', ')}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.store.showOnlyModels.length > 0) {
|
export function getAllSeries() {
|
||||||
logger.info(`ℹ selected models: ${config.store.showOnlyModels.map(entry => {
|
return Array.from(series);
|
||||||
return entry.series ? entry.name + ' (' + entry.series + ')' : entry.name;
|
|
||||||
}).join(', ')}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.store.showOnlySeries.length > 0) {
|
export function getAllModels() {
|
||||||
logger.info(`ℹ selected series: ${config.store.showOnlySeries.join(', ')}`);
|
return Array.from(models);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Stores = Array.from(list.values()) as Store[];
|
|
||||||
|
|
||||||
export * from './store';
|
export * from './store';
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import {IncomingMessage, Server, ServerResponse, createServer} from 'http';
|
||||||
|
import {config, setConfig} from '../config';
|
||||||
|
import {createReadStream, readdir} from 'fs';
|
||||||
|
import {getAllBrands, getAllModels, getAllSeries, storeList} from '../store/model';
|
||||||
|
import {join, normalize} from 'path';
|
||||||
|
|
||||||
|
const approot = join(__dirname, '../../');
|
||||||
|
const webroot = join(approot, './web');
|
||||||
|
|
||||||
|
const contentTypeMap: { [key: string]: string } = {
|
||||||
|
css: 'text/css',
|
||||||
|
htm: 'text/html',
|
||||||
|
html: 'text/html',
|
||||||
|
jpeg: 'image/jpeg',
|
||||||
|
jpg: 'image/jpeg',
|
||||||
|
js: 'application/javascript',
|
||||||
|
json: 'application/json',
|
||||||
|
png: 'image/png',
|
||||||
|
txt: 'text/plain'
|
||||||
|
};
|
||||||
|
|
||||||
|
function sendFile(response: ServerResponse, path: string, relativeTo: string = webroot) {
|
||||||
|
path = normalize(`./${path}`);
|
||||||
|
|
||||||
|
const fsPath = join(relativeTo, path);
|
||||||
|
try {
|
||||||
|
const stream = createReadStream(fsPath);
|
||||||
|
stream.on('error', error => {
|
||||||
|
sendError(response, error.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
const pathSplit = path.split('.');
|
||||||
|
const ext = pathSplit[pathSplit.length - 1].toLowerCase();
|
||||||
|
|
||||||
|
response.setHeader('Content-Type', contentTypeMap[ext] ?? contentTypeMap.txt);
|
||||||
|
|
||||||
|
stream.on('end', () => response.end());
|
||||||
|
stream.pipe(response);
|
||||||
|
} catch (error) {
|
||||||
|
sendError(response, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendError(response: ServerResponse, data: string, statusCode = 500) {
|
||||||
|
response.statusCode = statusCode;
|
||||||
|
response.setHeader('Content-Type', contentTypeMap.txt);
|
||||||
|
response.write(data);
|
||||||
|
response.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendJSON(response: ServerResponse, data: any) {
|
||||||
|
response.setHeader('Content-Type', contentTypeMap.json);
|
||||||
|
response.write(JSON.stringify(data));
|
||||||
|
response.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendConfig(response: ServerResponse) {
|
||||||
|
sendJSON(response, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAPI(request: IncomingMessage, response: ServerResponse, urlComponents: string[]) {
|
||||||
|
if (urlComponents.length < 2) {
|
||||||
|
sendError(response, 'No API route specified', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (urlComponents[1]) {
|
||||||
|
case 'config':
|
||||||
|
if (request.method === 'PUT') {
|
||||||
|
const data: string[] = [];
|
||||||
|
request.on('data', chunk => {
|
||||||
|
data.push(chunk);
|
||||||
|
});
|
||||||
|
request.on('end', () => {
|
||||||
|
// We ignore errors, client just sent wrong data...
|
||||||
|
try {
|
||||||
|
setConfig(JSON.parse(data.join('')));
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
sendConfig(response);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendConfig(response);
|
||||||
|
return;
|
||||||
|
case 'stores':
|
||||||
|
sendJSON(response, Array.from(storeList.keys()));
|
||||||
|
return;
|
||||||
|
case 'brands':
|
||||||
|
sendJSON(response, getAllBrands());
|
||||||
|
return;
|
||||||
|
case 'series':
|
||||||
|
sendJSON(response, getAllSeries());
|
||||||
|
return;
|
||||||
|
case 'models':
|
||||||
|
sendJSON(response, getAllModels());
|
||||||
|
return;
|
||||||
|
case 'screenshots':
|
||||||
|
if (urlComponents.length >= 3) {
|
||||||
|
const timeStamp = urlComponents[2];
|
||||||
|
if (/\D/.test(timeStamp)) {
|
||||||
|
sendError(response, 'Invalid screenshot timestamp', 400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendFile(response, `../success-${timeStamp}.png`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
readdir(approot, (error, files) => {
|
||||||
|
if (error) {
|
||||||
|
sendError(response, error.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
for (const file of files) {
|
||||||
|
const match = /^success-(\d+)\.png$/.exec(file);
|
||||||
|
if (!match) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push(match[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendJSON(response, result);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
sendError(response, 'No API route found for path', 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestListener(request: IncomingMessage, response: ServerResponse) {
|
||||||
|
const url = request.url!;
|
||||||
|
const urlComponents = url.slice(1).split('/'); // Remove the leading /
|
||||||
|
|
||||||
|
switch (urlComponents[0]) {
|
||||||
|
case 'api':
|
||||||
|
handleAPI(request, response, urlComponents);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sendFile(response, url === '/' ? '/index.html' : url);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let server: Server | undefined;
|
||||||
|
|
||||||
|
export async function startAPIServer() {
|
||||||
|
await stopAPIServer();
|
||||||
|
if (process.env.WEB_PORT) {
|
||||||
|
server = createServer(requestListener);
|
||||||
|
server.listen(Number(process.env.WEB_PORT));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function stopAPIServer() {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (server) {
|
||||||
|
server.close(resolve);
|
||||||
|
server = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}
|
||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es2019",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"lib": ["es6", "dom"],
|
"lib": ["dom", "es2019", "es2020.bigint", "es2020.string", "es2020.symbol.wellknown"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"outDir": "build",
|
"outDir": "build",
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
|
|||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>nvidia-snatcher control</title>
|
||||||
|
<script type="text/javascript">
|
||||||
|
let config;
|
||||||
|
let brands;
|
||||||
|
let stores;
|
||||||
|
let series;
|
||||||
|
let models;
|
||||||
|
|
||||||
|
function renderList(id, elements, selectionArray) {
|
||||||
|
const list = document.getElementById(id);
|
||||||
|
list.innerHTML = '';
|
||||||
|
for (const element of elements) {
|
||||||
|
const name = `${id}_${element}`;
|
||||||
|
const htmlElement = document.createElement('input');
|
||||||
|
htmlElement.type = 'checkbox';
|
||||||
|
htmlElement.value = element;
|
||||||
|
htmlElement.innerHTML = element;
|
||||||
|
htmlElement.name = name;
|
||||||
|
htmlElement.id = name;
|
||||||
|
if (selectionArray.length === 0 || selectionArray.includes(element)) {
|
||||||
|
htmlElement.checked = true;
|
||||||
|
}
|
||||||
|
list.appendChild(htmlElement);
|
||||||
|
const htmlLabelElement = document.createElement('label');
|
||||||
|
htmlLabelElement.for = name;
|
||||||
|
htmlLabelElement.innerText = element;
|
||||||
|
list.appendChild(htmlLabelElement);
|
||||||
|
list.appendChild(document.createElement('br'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listToArray(id, selectionArray) {
|
||||||
|
const list = document.getElementById(id);
|
||||||
|
const resArray = [];
|
||||||
|
let allSelected = true;
|
||||||
|
for (const htmlElement of list.childNodes) {
|
||||||
|
if (htmlElement.checked) {
|
||||||
|
resArray.push(htmlElement.value);
|
||||||
|
} else {
|
||||||
|
allSelected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allSelected) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return resArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onReceiveConfig(resp) {
|
||||||
|
config = await resp.json();
|
||||||
|
|
||||||
|
renderList('storeList', stores, config.store.stores);
|
||||||
|
renderList('brandList', brands, config.store.showOnlyBrands);
|
||||||
|
renderList('seriesList', series, config.store.showOnlySeries);
|
||||||
|
renderList('modelList', models, config.store.showOnlyModels);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setConfig() {
|
||||||
|
if (!config) {
|
||||||
|
throw new Error('Config not loaded yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
config.store.stores = listToArray('storeList');
|
||||||
|
config.store.showOnlyBrands = listToArray('brandList');
|
||||||
|
config.store.showOnlySeries = listToArray('seriesList');
|
||||||
|
config.store.showOnlyModels = listToArray('modelList');
|
||||||
|
|
||||||
|
const resp = await fetch('/api/config', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
});
|
||||||
|
|
||||||
|
await onReceiveConfig(resp);
|
||||||
|
|
||||||
|
alert('Saved!');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadScreenshots() {
|
||||||
|
const respScreenshots = await fetch('/api/screenshots');
|
||||||
|
const screenshots = await respScreenshots.json();
|
||||||
|
|
||||||
|
const screenshotContainer = document.getElementById('screenshots');
|
||||||
|
screenshotContainer.innerHTML = '';
|
||||||
|
|
||||||
|
for (const screenshot of screenshots) {
|
||||||
|
const htmlElement = document.createElement('a');
|
||||||
|
htmlElement.href = `/api/screenshots/${screenshot}`;
|
||||||
|
htmlElement.target = '_blank';
|
||||||
|
htmlElement.innerText = screenshot;
|
||||||
|
screenshotContainer.appendChild(htmlElement);
|
||||||
|
screenshotContainer.appendChild(document.createElement('br'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
const resp = await fetch('/api/config');
|
||||||
|
await onReceiveConfig(resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadInitial() {
|
||||||
|
const respStores = await fetch('/api/stores');
|
||||||
|
stores = await respStores.json();
|
||||||
|
|
||||||
|
const respBrands = await fetch('/api/brands');
|
||||||
|
brands = await respBrands.json();
|
||||||
|
|
||||||
|
const respSeries = await fetch('/api/series');
|
||||||
|
series = await respSeries.json();
|
||||||
|
|
||||||
|
const respModels = await fetch('/api/models');
|
||||||
|
models = await respModels.json();
|
||||||
|
|
||||||
|
await loadConfig();
|
||||||
|
|
||||||
|
await loadScreenshots();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body onload="loadInitial();">
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Stores</th>
|
||||||
|
<th>Brands</th>
|
||||||
|
<th>Series</th>
|
||||||
|
<th>Models</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td valign="top" id="storeList"></td>
|
||||||
|
<td valign="top" id="brandList"></td>
|
||||||
|
<td valign="top" id="seriesList"></td>
|
||||||
|
<td valign="top" id="modelList"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<input type="button" onclick="setConfig();" value="Save" />
|
||||||
|
|
||||||
|
<br /><br /><br /><br />
|
||||||
|
<b>Screenshots (<a href="javascript:loadScreenshots();">Refresh</a>)</b>
|
||||||
|
<div id="screenshots"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user