Added OpenAPI spec and UI

This commit is contained in:
Djuri Baars 2023-11-14 23:09:23 +01:00
parent b68c4a60e0
commit b2d07139c8
10 changed files with 695 additions and 62 deletions

View File

@ -3,6 +3,7 @@ import { sassPlugin } from "esbuild-sass-plugin";
import htmlPlugin from '@chialab/esbuild-plugin-html'; import htmlPlugin from '@chialab/esbuild-plugin-html';
import handlebarsPlugin from "esbuild-plugin-handlebars"; import handlebarsPlugin from "esbuild-plugin-handlebars";
import { clean } from 'esbuild-plugin-clean'; import { clean } from 'esbuild-plugin-clean';
import { copy } from 'esbuild-plugin-copy';
import postcss from "postcss"; import postcss from "postcss";
import autoprefixer from "autoprefixer"; import autoprefixer from "autoprefixer";
@ -11,7 +12,7 @@ const hbsOptions = {
additionalHelpers: { splitText: "helpers.js" }, additionalHelpers: { splitText: "helpers.js" },
additionalPartials: {}, additionalPartials: {},
precompileOptions: {} precompileOptions: {}
} }
esbuild esbuild
.build({ .build({
@ -29,7 +30,7 @@ esbuild
plugins: [ plugins: [
clean({ clean({
patterns: ['./build/*'] patterns: ['./build/*']
}), }),
htmlPlugin(), htmlPlugin(),
sassPlugin({ sassPlugin({
async transform(source) { async transform(source) {
@ -40,7 +41,15 @@ esbuild
}, },
}), }),
handlebarsPlugin(hbsOptions), handlebarsPlugin(hbsOptions),
copy({
// this is equal to process.cwd(), which means we use cwd path as base path to resolve `to` path
// if not specified, this plugin uses ESBuild.build outdir/outfile options as base path.
resolveFrom: 'cwd',
assets: {
from: ['./src/api.*'],
to: ['./build'],
},
})
], ],
minify: true, minify: true,
metafile: false, metafile: false,

View File

@ -14,6 +14,7 @@
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
"bootstrap": "^5.3.2", "bootstrap": "^5.3.2",
"esbuild-plugin-clean": "^1.0.1", "esbuild-plugin-clean": "^1.0.1",
"esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-handlebars": "^1.0.2", "esbuild-plugin-handlebars": "^1.0.2",
"esbuild-sass-plugin": "^2.16.0", "esbuild-sass-plugin": "^2.16.0",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",

31
data/src/api.html Normal file
View File

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta
name="description"
content="SwaggerUI"
/>
<title>BTClock API</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js" crossorigin></script>
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-standalone-preset.js" crossorigin></script>
<script>
window.onload = () => {
window.ui = SwaggerUIBundle({
url: 'api.json',
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
layout: "StandaloneLayout",
});
};
</script>
</body>
</html>

345
data/src/api.json Normal file
View File

@ -0,0 +1,345 @@
{
"openapi": "3.0.3",
"info": {
"title": "BTClock API",
"version": "3.0",
"description": "BTClock V3 API"
},
"servers": [
{
"url": "/api/"
}
],
"paths": {
"/status": {
"get": {
"tags": [
"system"
],
"summary": "Get current status",
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/system_status": {
"get": {
"tags": [
"system"
],
"summary": "Get system status",
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/settings": {
"get": {
"tags": [
"system"
],
"summary": "Get current settings",
"responses": {
"200": {
"description": "successful operation"
}
}
},
"post": {
"tags": [
"system"
],
"summary": "Save current settings",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Settings"
}
}
}
},
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/action/pause": {
"get": {
"tags": [
"timer"
],
"summary": "Pause screen rotation",
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/action/timer_restart": {
"get": {
"tags": [
"timer"
],
"summary": "Restart screen rotation",
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/show/screen/{id}": {
"get": {
"tags": [
"screens"
],
"summary": "Set screen to show",
"parameters": [
{
"in": "path",
"name": "id",
"schema": {
"type": "integer",
"default": 1
},
"required": true,
"description": "ID of screen to show"
}
],
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/show/text/{text}": {
"get": {
"tags": [
"screens"
],
"summary": "Set text to show",
"parameters": [
{
"in": "path",
"name": "text",
"schema": {
"type": "string",
"default": "text"
},
"required": true,
"description": "Text to show"
}
],
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/show/custom": {
"post": {
"tags": [
"screens"
],
"summary": "Set text to show (advanced)",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CustomText"
}
}
}
},
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/full_refresh": {
"get": {
"tags": [
"system"
],
"summary": "Force full refresh of all displays",
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/lights/{color}": {
"get": {
"tags": [
"lights"
],
"summary": "Turn on LEDs with specific color",
"parameters": [
{
"in": "path",
"name": "color",
"schema": {
"type": "string",
"default": "FFCC00"
},
"required": true,
"description": "Color in RGB hex"
}
],
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/lights/off": {
"get": {
"tags": [
"lights"
],
"summary": "Turn LEDs off",
"responses": {
"200": {
"description": "successful operation"
}
}
}
},
"/restart": {
"get": {
"tags": [
"system"
],
"summary": "Restart BTClock",
"responses": {
"200": {
"description": "successful operation"
}
}
}
}
},
"components": {
"schemas": {
"Settings": {
"type": "object",
"properties": {
"fetchEurPrice": {
"type": "boolean",
"description": "Fetch EUR price instead of USD"
},
"fgColor": {
"type": "string",
"default": 16777215,
"description": "ePaper foreground (text) color"
},
"bgColor": {
"type": "string",
"default": 0,
"description": "ePaper background color"
},
"ledTestOnPower": {
"type": "boolean",
"default": true,
"description": "Do LED test on power-on"
},
"ledFlashOnUpd": {
"type": "boolean",
"default": false,
"description": "Flash LEDs on new block"
},
"mdnsEnabled": {
"type": "boolean",
"default": true,
"description": "Enable mDNS"
},
"otaEnabled": {
"type": "boolean",
"default": true,
"description": "Enable over-the-air updates"
},
"stealFocusOnBlock": {
"type": "boolean",
"default": false,
"description": "Steal focus on new block"
},
"mcapBigChar": {
"type": "boolean",
"default": false,
"description": "Use big characters for market cap screen"
},
"mempoolInstance": {
"type": "string",
"default": "mempool.space",
"description": "Mempool.space instance to connect to"
},
"ledBrightness": {
"type": "integer",
"default": 128,
"description": "Brightness of LEDs"
},
"fullRefreshMin": {
"type": "integer",
"default": 60,
"description": "Full refresh time of ePaper displays in minutes"
},
"screen[0]": {
"type": "boolean"
},
"screen[1]": {
"type": "boolean"
},
"screen[2]": {
"type": "boolean"
},
"screen[3]": {
"type": "boolean"
},
"screen[4]": {
"type": "boolean"
},
"screen[5]": {
"type": "boolean"
},
"tzOffset": {
"type": "integer",
"default": 60,
"description": "Timezone offset in minutes"
},
"minSecPriceUpd": {
"type": "integer",
"default": 30,
"description": "Minimum time between price updates in seconds"
},
"timePerScreen": {
"type": "integer",
"default": 30,
"description": "Time between screens when rotating in minutes"
}
}
},
"CustomText": {
"type": "array",
"items": {
"type": "string"
},
"minItems": 7,
"maxItems": 7
}
}
}
}

View File

@ -87,7 +87,8 @@ div#output .btclock {
margin: 0 auto; margin: 0 auto;
.digit, .digit,
.splitText { .splitText,
.mediumText {
border: 2px solid gold; border: 2px solid gold;
border-radius: 8px; border-radius: 8px;

View File

@ -13,10 +13,18 @@
</head> </head>
<body> <body>
<nav class="navbar"> <nav class="navbar navbar-expand">
<div class="container-fluid"> <div class="container-fluid">
<span class="navbar-brand mb-0 h1">&#8383;TClock</span> <span class="navbar-brand mb-0 h1">&#8383;TClock</span>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" target="_blank" href="api.html">API</a>
</li>
</ul>
</div>
</div> </div>
</nav> </nav>
<script id="entry-template" type="text/x-handlebars-template"> <script id="entry-template" type="text/x-handlebars-template">
<div class="entry"> <div class="entry">
@ -181,7 +189,8 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<label for="minSecPriceUpd" class="col-sm-6 col-form-label col-form-label-sm">Time between price updates</label> <label for="minSecPriceUpd" class="col-sm-6 col-form-label col-form-label-sm">Time between price
updates</label>
<div class="col-sm-6 mb-3"> <div class="col-sm-6 mb-3">
<div class="input-group input-group-sm"> <div class="input-group input-group-sm">
<input type="number" name="minSecPriceUpd" min="1" id="minSecPriceUpd" class="form-control"> <input type="number" name="minSecPriceUpd" min="1" id="minSecPriceUpd" class="form-control">
@ -201,7 +210,7 @@
<div class="form-text">A restart is required to apply TZ offset.</div> <div class="form-text">A restart is required to apply TZ offset.</div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<label class="col-sm-6 col-form-label col-form-label-sm" for="ledBrightness">LED brightness</label> <label class="col-sm-6 col-form-label col-form-label-sm" for="ledBrightness">LED brightness</label>
<div class="col-sm-6"> <div class="col-sm-6">

227
data/src/swagger.yaml Normal file
View File

@ -0,0 +1,227 @@
openapi: 3.0.3
info:
title: BTClock API
version: "3.0"
description: BTClock V3 API
servers:
- url: /api/
paths:
/status:
get:
tags:
- system
summary: Get current status
responses:
'200':
description: successful operation
/system_status:
get:
tags:
- system
summary: Get system status
responses:
'200':
description: successful operation
/settings:
get:
tags:
- system
summary: Get current settings
responses:
'200':
description: successful operation
post:
tags:
- system
summary: Save current settings
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/Settings'
responses:
'200':
description: successful operation
/action/pause:
get:
tags:
- timer
summary: Pause screen rotation
responses:
'200':
description: successful operation
/action/timer_restart:
get:
tags:
- timer
summary: Restart screen rotation
responses:
'200':
description: successful operation
/show/screen/{id}:
get:
tags:
- screens
summary: Set screen to show
parameters:
- in: path
name: id
schema:
type: integer
default: 1
required: true
description: ID of screen to show
responses:
'200':
description: successful operation
/show/text/{text}:
get:
tags:
- screens
summary: Set text to show
parameters:
- in: path
name: text
schema:
type: string
default: "text"
required: true
description: Text to show
responses:
'200':
description: successful operation
/show/custom:
post:
tags:
- screens
summary: Set text to show (advanced)
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CustomText'
responses:
'200':
description: successful operation
/full_refresh:
get:
tags:
- system
summary: Force full refresh of all displays
responses:
'200':
description: successful operation
/lights/{color}:
get:
tags:
- lights
summary: Turn on LEDs with specific color
parameters:
- in: path
name: color
schema:
type: string
default: FFCC00
required: true
description: Color in RGB hex
responses:
'200':
description: successful operation
/lights/off:
get:
tags:
- lights
summary: Turn LEDs off
responses:
'200':
description: successful operation
/restart:
get:
tags:
- system
summary: Restart BTClock
responses:
'200':
description: successful operation
components:
schemas:
Settings:
type: object
properties:
fetchEurPrice:
type: boolean
description: Fetch EUR price instead of USD
fgColor:
type: string
default: 0xFFFFFF
description: ePaper foreground (text) color
bgColor:
type: string
default: 0x000000
description: ePaper background color
ledTestOnPower:
type: boolean
default: true
description: Do LED test on power-on
ledFlashOnUpd:
type: boolean
default: false
description: Flash LEDs on new block
mdnsEnabled:
type: boolean
default: true
description: Enable mDNS
otaEnabled:
type: boolean
default: true
description: Enable over-the-air updates
stealFocusOnBlock:
type: boolean
default: false
description: Steal focus on new block
mcapBigChar:
type: boolean
default: false
description: Use big characters for market cap screen
mempoolInstance:
type: string
default: mempool.space
description: Mempool.space instance to connect to
ledBrightness:
type: integer
default: 128
description: Brightness of LEDs
fullRefreshMin:
type: integer
default: 60
description: Full refresh time of ePaper displays in minutes
screen[0]:
type: boolean
screen[1]:
type: boolean
screen[2]:
type: boolean
screen[3]:
type: boolean
screen[4]:
type: boolean
screen[5]:
type: boolean
tzOffset:
type: integer
default: 60
description: Timezone offset in minutes
minSecPriceUpd:
type: integer
default: 30
description: Minimum time between price updates in seconds
timePerScreen:
type: integer
default: 30
description: Time between screens when rotating in minutes
CustomText:
type: array
items:
type: string
minItems: 7
maxItems: 7

View File

@ -1,48 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<link href="/css/style.css" rel="stylesheet">
<title>&#8383;TClock WiFi Settings</title>
</head>
<body>
<nav class="navbar navbar-light bg-light">
<div class="container-fluid">
<span class="navbar-brand mb-0 h1">&#8383;TClock WiFi Settings</span>
</div>
</nav>
<div class="container-fluid">
<div class="row">
<div class="col">
<div class="h-100 p-3 border bg-light">
<h1>WiFi Settings</h1>
<form name="customText" id="customTextForm" method="post" action="/setup/wifi">
<div class="row">
<label for="ssid" class="col-sm-4 col-form-label">SSID</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="ssid" name="ssid" required>
</div>
</div>
<div class="row">
<label for="password" class="col-sm-4 col-form-label">Password</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="password" name="password" required>
</div>
</div>
<footer>
<button type="submit" class="btn btn-primary">Save and connect</button>
<p><small>The BTClock will restart and connect to your network. If it doesn't, reset to factory settings by holding the red button while booting to retry.</small></p>
</footer>
</form>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -610,7 +610,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"chokidar@npm:>=3.0.0 <4.0.0": "chokidar@npm:>=3.0.0 <4.0.0, chokidar@npm:^3.5.3":
version: 3.5.3 version: 3.5.3
resolution: "chokidar@npm:3.5.3" resolution: "chokidar@npm:3.5.3"
dependencies: dependencies:
@ -767,6 +767,7 @@ __metadata:
bootstrap: ^5.3.2 bootstrap: ^5.3.2
esbuild: 0.19.4 esbuild: 0.19.4
esbuild-plugin-clean: ^1.0.1 esbuild-plugin-clean: ^1.0.1
esbuild-plugin-copy: ^2.1.1
esbuild-plugin-handlebars: ^1.0.2 esbuild-plugin-handlebars: ^1.0.2
esbuild-sass-plugin: ^2.16.0 esbuild-sass-plugin: ^2.16.0
handlebars: ^4.7.7 handlebars: ^4.7.7
@ -959,6 +960,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"esbuild-plugin-copy@npm:^2.1.1":
version: 2.1.1
resolution: "esbuild-plugin-copy@npm:2.1.1"
dependencies:
chalk: ^4.1.2
chokidar: ^3.5.3
fs-extra: ^10.0.1
globby: ^11.0.3
peerDependencies:
esbuild: ">= 0.14.0"
checksum: d0122fa059e1cf8b80c3aa2cba1dee94ca28a992bb28ea5a4690399aa1fefdc6522c177e90a8373aa676c5fbd878f234a72cc004207c1262a6d334277a1e1368
languageName: node
linkType: hard
"esbuild-plugin-handlebars@npm:^1.0.2": "esbuild-plugin-handlebars@npm:^1.0.2":
version: 1.0.2 version: 1.0.2
resolution: "esbuild-plugin-handlebars@npm:1.0.2" resolution: "esbuild-plugin-handlebars@npm:1.0.2"
@ -1172,6 +1187,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fs-extra@npm:^10.0.1":
version: 10.1.0
resolution: "fs-extra@npm:10.1.0"
dependencies:
graceful-fs: ^4.2.0
jsonfile: ^6.0.1
universalify: ^2.0.0
checksum: dc94ab37096f813cc3ca12f0f1b5ad6744dfed9ed21e953d72530d103cea193c2f81584a39e9dee1bea36de5ee66805678c0dddc048e8af1427ac19c00fffc50
languageName: node
linkType: hard
"fs-minipass@npm:^2.0.0": "fs-minipass@npm:^2.0.0":
version: 2.1.0 version: 2.1.0
resolution: "fs-minipass@npm:2.1.0" resolution: "fs-minipass@npm:2.1.0"
@ -1277,7 +1303,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"globby@npm:^11.0.1": "globby@npm:^11.0.1, globby@npm:^11.0.3":
version: 11.1.0 version: 11.1.0
resolution: "globby@npm:11.1.0" resolution: "globby@npm:11.1.0"
dependencies: dependencies:
@ -1291,7 +1317,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6": "graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6":
version: 4.2.11 version: 4.2.11
resolution: "graceful-fs@npm:4.2.11" resolution: "graceful-fs@npm:4.2.11"
checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7
@ -1614,6 +1640,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"jsonfile@npm:^6.0.1":
version: 6.1.0
resolution: "jsonfile@npm:6.1.0"
dependencies:
graceful-fs: ^4.1.6
universalify: ^2.0.0
dependenciesMeta:
graceful-fs:
optional: true
checksum: 7af3b8e1ac8fe7f1eccc6263c6ca14e1966fcbc74b618d3c78a0a2075579487547b94f72b7a1114e844a1e15bb00d440e5d1720bfc4612d790a6f285d5ea8354
languageName: node
linkType: hard
"lodash._reinterpolate@npm:^3.0.0": "lodash._reinterpolate@npm:^3.0.0":
version: 3.0.0 version: 3.0.0
resolution: "lodash._reinterpolate@npm:3.0.0" resolution: "lodash._reinterpolate@npm:3.0.0"
@ -2522,6 +2561,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"universalify@npm:^2.0.0":
version: 2.0.1
resolution: "universalify@npm:2.0.1"
checksum: ecd8469fe0db28e7de9e5289d32bd1b6ba8f7183db34f3bfc4ca53c49891c2d6aa05f3fb3936a81285a905cc509fb641a0c3fc131ec786167eff41236ae32e60
languageName: node
linkType: hard
"update-browserslist-db@npm:^1.0.13": "update-browserslist-db@npm:^1.0.13":
version: 1.0.13 version: 1.0.13
resolution: "update-browserslist-db@npm:1.0.13" resolution: "update-browserslist-db@npm:1.0.13"

View File

@ -12,7 +12,6 @@ void setupWebserver()
return; return;
} }
events.onConnect([](AsyncEventSourceClient *client) events.onConnect([](AsyncEventSourceClient *client)
{ client->send("welcome", NULL, millis(), 1000); }); { client->send("welcome", NULL, millis(), 1000); });
server.addHandler(&events); server.addHandler(&events);
@ -20,6 +19,8 @@ void setupWebserver()
server.serveStatic("/css", LittleFS, "/css/"); server.serveStatic("/css", LittleFS, "/css/");
server.serveStatic("/js", LittleFS, "/js/"); server.serveStatic("/js", LittleFS, "/js/");
server.serveStatic("/font", LittleFS, "/font/"); server.serveStatic("/font", LittleFS, "/font/");
server.serveStatic("/api.json", LittleFS, "/api.json");
server.serveStatic("/api.html", LittleFS, "/api.html");
server.on("/", HTTP_GET, onIndex); server.on("/", HTTP_GET, onIndex);
@ -52,6 +53,8 @@ void setupWebserver()
server.onNotFound(onNotFound); server.onNotFound(onNotFound);
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
DefaultHeaders::Instance().addHeader("Access-Control-Allow-Headers", "*");
server.begin(); server.begin();
@ -177,6 +180,10 @@ void onApiFullRefresh(AsyncWebServerRequest *request)
request->send(200); request->send(200);
} }
/**
* @Api
* @Path("/api/show/screen")
*/
void onApiShowScreen(AsyncWebServerRequest *request) void onApiShowScreen(AsyncWebServerRequest *request)
{ {
if (request->hasParam("s")) if (request->hasParam("s"))
@ -523,9 +530,12 @@ void onApiLightsSetColor(AsyncWebServerRequest *request)
{ {
String rgbColor = request->getParam("c")->value(); String rgbColor = request->getParam("c")->value();
if (rgbColor.compareTo("off") == 0) { if (rgbColor.compareTo("off") == 0)
{
setLights(0, 0, 0); setLights(0, 0, 0);
} else { }
else
{
uint r, g, b; uint r, g, b;
sscanf(rgbColor.c_str(), "%02x%02x%02x", &r, &g, &b); sscanf(rgbColor.c_str(), "%02x%02x%02x", &r, &g, &b);
setLights(r, g, b); setLights(r, g, b);
@ -538,7 +548,9 @@ void onIndex(AsyncWebServerRequest *request) { request->send(LittleFS, "/index.h
void onNotFound(AsyncWebServerRequest *request) void onNotFound(AsyncWebServerRequest *request)
{ {
if (request->method() == HTTP_OPTIONS) // Access-Control-Request-Method == POST might be better
if (request->method() == HTTP_OPTIONS || request->hasHeader("Sec-Fetch-Mode"))
{ {
request->send(200); request->send(200);
} }