Error handling

This commit is contained in:
Joseph Abbey
2024-01-25 10:40:27 +00:00
parent c4066e9fe3
commit 2afd0295b4
2 changed files with 389 additions and 245 deletions

View File

@ -121,6 +121,11 @@
border-radius: 0.25em; border-radius: 0.25em;
padding: 0.25em; padding: 0.25em;
&::placeholder {
color: var(--ctp-mocha-text);
opacity: 0.4;
}
&:focus-visible { &:focus-visible {
outline: none; outline: none;
border: 1px solid var(--ctp-mocha-teal); border: 1px solid var(--ctp-mocha-teal);
@ -169,14 +174,19 @@
button.icon { button.icon {
border: none; border: none;
padding: 0; padding: 0.1em;
margin: 0; margin: 0;
width: 1.5em; width: 1.8em;
height: 1.5em; height: 1.8em;
background-color: transparent; background-color: var(--ctp-mocha-surface1);
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
&:hover { &:hover {
background-color: var(--ctp-mocha-red); background-color: var(--ctp-mocha-overlay1);
} }
&::before { &::before {
@ -191,10 +201,18 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
&[icon='close']::before { &[icon='close'] {
&:hover {
background-color: var(--ctp-mocha-red);
}
&::before {
background-image: url(https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/close/default/48px.svg); background-image: url(https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/close/default/48px.svg);
} }
} }
&[icon='download']::before {
background-image: url(https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/download/default/48px.svg);
}
}
} }
</style> </style>
</head> </head>
@ -212,7 +230,8 @@
type="url" type="url"
name="menu_url" name="menu_url"
id="menu_url" id="menu_url"
pattern="https://.*" /> pattern="https://.*\.json" />
<button class="icon" icon="download" id="download" type="button"></button>
<input <input
required required
autocomplete="new-password" autocomplete="new-password"

View File

@ -1,10 +1,13 @@
let api_url = localStorage.getItem('api_url') ?? '';
let menu_url = localStorage.getItem('menu_url') ?? '';
let api_token = localStorage.getItem('api_token') ?? '';
/** /**
* Get all entities in HomeAssistant. * Get all entities in HomeAssistant.
* @param {string} api_url
* @param {string} api_token
* @returns {Promise<Record<string, string>>} [id, name] * @returns {Promise<Record<string, string>>} [id, name]
*/ */
async function get_entities(api_url, api_token) { async function get_entities() {
try {
const res = await fetch(api_url + '/template', { const res = await fetch(api_url + '/template', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -15,17 +18,23 @@ async function get_entities(api_url, api_token) {
}); });
if (res.status == 401 || res.status == 403) { if (res.status == 401 || res.status == 403) {
document.querySelector('#api_token').classList.add('invalid'); document.querySelector('#api_token').classList.add('invalid');
return {};
} }
document.querySelector('#api_url').classList.remove('invalid');
document.querySelector('#api_token').classList.remove('invalid');
return Object.fromEntries(await res.json()); return Object.fromEntries(await res.json());
} catch {
document.querySelector('#api_url').classList.add('invalid');
return {};
}
} }
/** /**
* Get all devices in HomeAssistant. * Get all devices in HomeAssistant.
* @param {string} api_url
* @param {string} api_token
* @returns {Promise<Record<string, string>>} [id, name] * @returns {Promise<Record<string, string>>} [id, name]
*/ */
async function get_devices(api_url, api_token) { async function get_devices() {
try {
const res = await fetch(api_url + '/template', { const res = await fetch(api_url + '/template', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -36,17 +45,23 @@ async function get_devices(api_url, api_token) {
}); });
if (res.status == 401 || res.status == 403) { if (res.status == 401 || res.status == 403) {
document.querySelector('#api_token').classList.add('invalid'); document.querySelector('#api_token').classList.add('invalid');
return {};
} }
document.querySelector('#api_url').classList.remove('invalid');
document.querySelector('#api_token').classList.remove('invalid');
return Object.fromEntries(await res.json()); return Object.fromEntries(await res.json());
} catch {
document.querySelector('#api_url').classList.add('invalid');
return {};
}
} }
/** /**
* Get all areas in HomeAssistant. * Get all areas in HomeAssistant.
* @param {string} api_url
* @param {string} api_token
* @returns {Promise<Record<string, string>>} [id, name] * @returns {Promise<Record<string, string>>} [id, name]
*/ */
async function get_areas(api_url, api_token) { async function get_areas() {
try {
const res = await fetch(api_url + '/template', { const res = await fetch(api_url + '/template', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -57,17 +72,23 @@ async function get_areas(api_url, api_token) {
}); });
if (res.status == 401 || res.status == 403) { if (res.status == 401 || res.status == 403) {
document.querySelector('#api_token').classList.add('invalid'); document.querySelector('#api_token').classList.add('invalid');
return {};
} }
document.querySelector('#api_url').classList.remove('invalid');
document.querySelector('#api_token').classList.remove('invalid');
return Object.fromEntries(await res.json()); return Object.fromEntries(await res.json());
} catch {
document.querySelector('#api_url').classList.add('invalid');
return {};
}
} }
/** /**
* Get all services in HomeAssistant. * Get all services in HomeAssistant.
* @param {string} api_url
* @param {string} api_token
* @returns {Promise<[string, { name: string; description: string; fields: Record<string, { name: string; description: string; example: string; selector: unknown; required?: boolean }> }][]>} [id, data] * @returns {Promise<[string, { name: string; description: string; fields: Record<string, { name: string; description: string; example: string; selector: unknown; required?: boolean }> }][]>} [id, data]
*/ */
async function get_services(api_url, api_token) { async function get_services() {
try {
const res = await fetch(api_url + '/services', { const res = await fetch(api_url + '/services', {
method: 'GET', method: 'GET',
headers: { headers: {
@ -77,7 +98,10 @@ async function get_services(api_url, api_token) {
}); });
if (res.status == 401 || res.status == 403) { if (res.status == 401 || res.status == 403) {
document.querySelector('#api_token').classList.add('invalid'); document.querySelector('#api_token').classList.add('invalid');
return {};
} }
document.querySelector('#api_url').classList.remove('invalid');
document.querySelector('#api_token').classList.remove('invalid');
const data = await res.json(); const data = await res.json();
const services = []; const services = [];
for (const d of data) { for (const d of data) {
@ -86,6 +110,10 @@ async function get_services(api_url, api_token) {
} }
} }
return services; return services;
} catch {
document.querySelector('#api_url').classList.add('invalid');
return [];
}
} }
/** /**
@ -343,69 +371,6 @@ async function generate_schema(entities, devices, areas, services, schema) {
return schema; return schema;
} }
let api_url = localStorage.getItem('api_url') ?? '';
let menu_url = localStorage.getItem('menu_url') ?? '';
let api_token = localStorage.getItem('api_token') ?? '';
document.querySelector('#api_url').value = api_url;
document.querySelector('#menu_url').value = menu_url;
document.querySelector('#api_token').value = api_token;
document.querySelector('#troubleshooting').addEventListener('click', (e) => {
document.querySelector('#troubleshooting-dialog').showModal();
});
document.querySelector('#test-api').addEventListener('click', async (e) => {
try {
document.querySelector('#test-api-response').innerText = 'Testing...';
const res = await fetch(api_url + '/', {
headers: {
Authorization: `Bearer ${api_token}`,
},
cache: 'no-cache',
mode: 'cors',
});
const text = await res.text();
if (res.status == 200) {
document.querySelector('#test-api-response').innerText =
JSON.parse(text).message;
} else if (res.status == 400) {
try {
document.querySelector('#test-api-response').innerText =
JSON.parse(text).message;
} catch {
document.querySelector('#test-api-response').innerText = text;
}
} else if (res.status == 401 || res.status == 403) {
document.querySelector('#api_token').classList.add('invalid');
document.querySelector('#test-api-response').innerText = 'Invalid token.';
} else {
document.querySelector('#test-api-response').innerText = text;
}
} catch (e) {
document.querySelector('#test-api-response').innerText = e.message;
}
});
document.querySelector('#test-menu').addEventListener('click', async (e) => {
try {
document.querySelector('#test-menu-response').innerText = 'Testing...';
const res = await fetch(menu_url, {
cache: 'no-cache',
mode: 'cors',
});
if (res.status == 200) {
document.querySelector('#test-menu-response').innerText = 'Available';
} else if (res.status == 400) {
document.querySelector('#test-menu-response').innerText =
await res.text();
} else {
document.querySelector('#test-menu-response').innerText =
await res.text();
}
} catch (e) {
document.querySelector('#test-menu-response').innerText = e.message;
}
});
function get(d, p) { function get(d, p) {
for (let i = 0; i < p.length; i++) { for (let i = 0; i < p.length; i++) {
d = d[p[i]]; d = d[p[i]];
@ -432,6 +397,38 @@ function toast({ text, color }) {
return t; return t;
} }
let entities, devices, areas, services, schema;
async function loadSchema() {
[entities, devices, areas, services, schema] = await Promise.all([
get_entities(),
get_devices(),
get_areas(),
get_services(),
get_schema(),
]);
if (window.makeMarkers) {
window.makeMarkers();
}
try {
schema = await generate_schema(entities, devices, areas, services, schema);
} catch {}
console.log(schema);
if (window.m && window.modelUri) {
// configure the JSON language support with schemas and schema associations
window.m.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true,
schemas: [
{
uri: 'https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json',
fileMatch: [window.modelUri.toString()],
schema,
},
],
});
}
}
loadSchema();
// require is provided by loader.min.js. // require is provided by loader.min.js.
require.config({ require.config({
paths: { paths: {
@ -439,26 +436,11 @@ require.config({
}, },
}); });
require(['vs/editor/editor.main'], async () => { require(['vs/editor/editor.main'], async () => {
let entities, devices, areas, services, schema; window.m = monaco;
async function loadSchema() { var modelUri = monaco.Uri.parse('/config/www/garmin/menu.json'); // a made up unique URI for our model
[entities, devices, areas, services, schema] = await Promise.all([ window.modelUri = modelUri;
get_entities(api_url, api_token),
get_devices(api_url, api_token), if (schema) {
get_areas(api_url, api_token),
get_services(api_url, api_token),
get_schema(),
]);
makeMarkers();
try {
schema = await generate_schema(
entities,
devices,
areas,
services,
schema
);
} catch {}
console.log(schema);
// configure the JSON language support with schemas and schema associations // configure the JSON language support with schemas and schema associations
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
validate: true, validate: true,
@ -472,6 +454,110 @@ require(['vs/editor/editor.main'], async () => {
}); });
} }
document.querySelector('#api_url').value = api_url;
document.querySelector('#menu_url').value = menu_url;
document.querySelector('#api_token').value = api_token;
document.querySelector('#troubleshooting').addEventListener('click', (e) => {
document.querySelector('#troubleshooting-dialog').showModal();
});
document.querySelector('#test-api').addEventListener('click', async (e) => {
try {
document.querySelector('#test-api-response').innerText = 'Testing...';
const res = await fetch(api_url + '/', {
headers: {
Authorization: `Bearer ${api_token}`,
},
cache: 'no-cache',
mode: 'cors',
});
const text = await res.text();
if (res.status == 200) {
document.querySelector('#test-api-response').innerText =
JSON.parse(text).message;
document.querySelector('#api_token').classList.remove('invalid');
document.querySelector('#api_url').classList.remove('invalid');
} else if (res.status == 400) {
document.querySelector('#api_url').classList.add('invalid');
try {
document.querySelector('#test-api-response').innerText =
JSON.parse(text).message;
} catch {
document.querySelector('#test-api-response').innerText = text;
}
} else if (res.status == 401 || res.status == 403) {
document.querySelector('#api_token').classList.add('invalid');
document.querySelector('#test-api-response').innerText =
'Invalid token.';
} else {
document.querySelector('#test-api-response').innerText = text;
}
} catch (e) {
document.querySelector('#test-api-response').innerText =
'Check CORS settings on HomeAssistant server.';
document.querySelector('#api_token').classList.add('invalid');
}
});
document.querySelector('#test-menu').addEventListener('click', async (e) => {
try {
document.querySelector('#test-menu-response').innerText = 'Testing...';
const res = await fetch(menu_url, {
cache: 'no-cache',
mode: 'cors',
});
if (res.status == 200) {
document.querySelector('#menu_url').classList.remove('invalid');
document.querySelector('#test-menu-response').innerText = 'Available';
} else if (res.status == 400) {
document.querySelector('#menu_url').classList.add('invalid');
document.querySelector('#test-menu-response').innerText =
await res.text();
} else {
document.querySelector('#menu_url').classList.add('invalid');
document.querySelector('#test-menu-response').innerText =
await res.text();
}
} catch (e) {
document.querySelector('#menu_url').classList.add('invalid');
document.querySelector('#test-menu-response').innerText =
'Check CORS settings on HomeAssistant server.';
}
});
document.querySelector('#download').addEventListener('click', async (e) => {
try {
const t = toast({
text: 'Downloading...',
});
const res = await fetch(menu_url, {
cache: 'no-cache',
mode: 'cors',
});
t.hideToast();
if (res.status == 200) {
document.querySelector('#menu_url').classList.remove('invalid');
const text = await res.text();
model.setValue(text);
toast({
text: 'Downloaded!',
color: 'var(--ctp-mocha-green)',
});
} else {
document.querySelector('#menu_url').classList.add('invalid');
toast({
text: await res.text(),
color: 'var(--ctp-mocha-red)',
});
}
} catch (e) {
toast({
text: 'Check CORS settings on HomeAssistant server.',
color: 'var(--ctp-mocha-red)',
});
document.querySelector('#menu_url').classList.add('invalid');
}
});
document.querySelector('#api_url').addEventListener('change', (e) => { document.querySelector('#api_url').addEventListener('change', (e) => {
api_url = e.target.value; api_url = e.target.value;
localStorage.setItem('api_url', api_url); localStorage.setItem('api_url', api_url);
@ -491,17 +577,18 @@ require(['vs/editor/editor.main'], async () => {
document.querySelector('#test-api-response').innerText = 'Check now!'; document.querySelector('#test-api-response').innerText = 'Check now!';
loadSchema(); loadSchema();
}); });
loadSchema();
checkRemoteMenu(); checkRemoteMenu();
async function checkRemoteMenu() { async function checkRemoteMenu() {
if (menu_url != '') { if (menu_url != '') {
try {
const remote = await fetch(menu_url, { const remote = await fetch(menu_url, {
cache: 'no-cache', cache: 'no-cache',
mode: 'cors', mode: 'cors',
}); });
if (remote.status == 200) { if (remote.status == 200) {
document.querySelector('#menu_url').classList.remove('invalid'); document.querySelector('#menu_url').classList.remove('invalid');
document.querySelector('#download').disabled = false;
const text = await remote.text(); const text = await remote.text();
if (model.getValue() === text) { if (model.getValue() === text) {
document.querySelector('#menu_url').classList.remove('outofsync'); document.querySelector('#menu_url').classList.remove('outofsync');
@ -510,13 +597,20 @@ require(['vs/editor/editor.main'], async () => {
} }
} else { } else {
document.querySelector('#menu_url').classList.add('invalid'); document.querySelector('#menu_url').classList.add('invalid');
document.querySelector('#download').disabled = true;
} }
} catch {
document.querySelector('#menu_url').classList.add('invalid');
document.querySelector('#download').disabled = true;
}
} else {
document.querySelector('#menu_url').classList.remove('invalid');
document.querySelector('#download').disabled = true;
} }
} }
// Configures two JSON schemas, with references. setInterval(checkRemoteMenu, 30000);
var modelUri = monaco.Uri.parse('/config/www/garmin/menu.json'); // a made up unique URI for our model
var model = monaco.editor.createModel( var model = monaco.editor.createModel(
localStorage.getItem('json') ?? '{}', localStorage.getItem('json') ?? '{}',
'json', 'json',
@ -556,6 +650,7 @@ require(['vs/editor/editor.main'], async () => {
const t = toast({ const t = toast({
text: 'Rendering template...', text: 'Rendering template...',
}); });
try {
const res = await fetch(api_url + '/template', { const res = await fetch(api_url + '/template', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -583,6 +678,14 @@ require(['vs/editor/editor.main'], async () => {
color: 'var(--ctp-mocha-red)', color: 'var(--ctp-mocha-red)',
}); });
} }
} catch (e) {
t.hideToast();
toast({
text: 'Check CORS settings on HomeAssistant server.',
color: 'var(--ctp-mocha-red)',
});
document.querySelector('#api_url').classList.add('invalid');
}
}, },
'' ''
); );
@ -602,6 +705,7 @@ require(['vs/editor/editor.main'], async () => {
const t = toast({ const t = toast({
text: 'Running action...', text: 'Running action...',
}); });
try {
const res = await fetch( const res = await fetch(
api_url + '/services/' + service[0] + '/' + service[1], api_url + '/services/' + service[0] + '/' + service[1],
{ {
@ -641,6 +745,14 @@ require(['vs/editor/editor.main'], async () => {
}); });
} }
makeMarkers(); makeMarkers();
} catch (e) {
t.hideToast();
toast({
text: 'Check CORS settings on HomeAssistant server.',
color: 'var(--ctp-mocha-red)',
});
document.querySelector('#api_url').classList.add('invalid');
}
}, },
'' ''
); );
@ -652,7 +764,10 @@ require(['vs/editor/editor.main'], async () => {
const t = toast({ const t = toast({
text: 'Toggling...', text: 'Toggling...',
}); });
const res = await fetch(api_url + '/services/' + entity[0] + '/toggle', { try {
const res = await fetch(
api_url + '/services/' + entity[0] + '/toggle',
{
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: `Bearer ${api_token}`, Authorization: `Bearer ${api_token}`,
@ -661,7 +776,8 @@ require(['vs/editor/editor.main'], async () => {
body: JSON.stringify({ body: JSON.stringify({
entity_id: item.entity, entity_id: item.entity,
}), }),
}); }
);
t.hideToast(); t.hideToast();
if (res.status == 200) { if (res.status == 200) {
toast({ toast({
@ -690,6 +806,14 @@ require(['vs/editor/editor.main'], async () => {
}); });
} }
makeMarkers(); makeMarkers();
} catch (e) {
t.hideToast();
toast({
text: 'Check CORS settings on HomeAssistant server.',
color: 'var(--ctp-mocha-red)',
});
document.querySelector('#api_url').classList.add('invalid');
}
}, },
'' ''
); );
@ -854,6 +978,7 @@ require(['vs/editor/editor.main'], async () => {
monaco.editor.setModelMarkers(model, 'template', markers); monaco.editor.setModelMarkers(model, 'template', markers);
} catch {} } catch {}
} }
window.makeMarkers = makeMarkers;
makeMarkers(); makeMarkers();
model.onDidChangeContent(async function () { model.onDidChangeContent(async function () {