Implemented guests cache. Implemented guests function within unifi.js. Updated app screenshots. Updated manifest.json. Added details.ejs component. Implemented voucher detail slide-over. Updated cache.js logic to fetch guests. Updated README.md. Implemented voucher detail component route.

This commit is contained in:
Glenn de Haan
2024-04-25 21:10:46 +02:00
parent 72d6f4df3c
commit f68ee13ec0
16 changed files with 296 additions and 12 deletions

View File

@@ -4,7 +4,7 @@ A small UniFi Voucher Site for simple voucher creation
[![Image Size](https://img.shields.io/docker/image-size/glenndehaan/unifi-voucher-site)](https://hub.docker.com/r/glenndehaan/unifi-voucher-site) [![Image Size](https://img.shields.io/docker/image-size/glenndehaan/unifi-voucher-site)](https://hub.docker.com/r/glenndehaan/unifi-voucher-site)
![Vouchers Overview - Desktop](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/2bc18cce-afb2-41e9-a34c-d923fab9f6e4) ![Vouchers Overview - Desktop](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/b0d5c208-2ac7-444e-977d-31287ff19e8b)
## Structure ## Structure
@@ -233,22 +233,28 @@ The application will automatically format the voucher for 80mm paper width, ensu
## Screenshots ## Screenshots
### Login (Desktop) ### Login (Desktop)
![Login - Desktop](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/89289053-50e0-4169-9916-2bce7191bf49) ![Login - Desktop](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/5f89ecbd-7e03-4fd0-ae7d-279d16321384)
### Vouchers Overview (Desktop) ### Vouchers Overview (Desktop)
![Vouchers Overview - Desktop](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/2bc18cce-afb2-41e9-a34c-d923fab9f6e4) ![Vouchers Overview - Desktop](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/b0d5c208-2ac7-444e-977d-31287ff19e8b)
### Create Voucher (Desktop) ### Create Voucher (Desktop)
![Create Voucher - Desktop](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/32803e88-c3cb-4708-914c-7e7184eae1c6) ![Create Voucher - Desktop](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/72f8dcf0-6642-4c89-849f-21cfbcc488ab)
### Voucher Details (Desktop)
![Voucher Details - Desktop](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/b84ad74c-afaa-4bf1-8bc1-398fb0450ff1)
### Login (Mobile) ### Login (Mobile)
![Login - Mobile](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/c236bb65-1ef5-41dd-976f-8f72a98416a3) ![Login - Mobile](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/d74bc487-5b80-4bb6-8617-da870cdf4cec)
### Vouchers Overview (Mobile) ### Vouchers Overview (Mobile)
![Voucher Overview - Mobile](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/68656bad-b4a2-495d-baae-61a64ed52e72) ![Voucher Overview - Mobile](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/c986e03d-5edf-4b04-8903-0b42ff1c4fc9)
### Create Voucher (Mobile) ### Create Voucher (Mobile)
![Create Voucher - Mobile](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/b393c3d9-1e87-40e7-8998-9607872b161f) ![Create Voucher - Mobile](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/f1cef8c8-a7a5-4238-8a2e-461835375f29)
### Voucher Details (Mobile)
![Voucher Details - Mobile](https://github.com/glenndehaan/unifi-voucher-site/assets/7496187/28b8f97b-8042-4e6d-b1dc-8386860a1e39)
## License ## License

View File

@@ -1,9 +1,10 @@
/** /**
* Internal application cache * Internal application cache
* *
* @type {{vouchers: *[], updated: number}} * @type {{guests: *[], vouchers: *[], updated: number}}
*/ */
module.exports = { module.exports = {
vouchers: [], vouchers: [],
guests: [],
updated: 0 updated: 0
}; };

View File

@@ -216,6 +216,52 @@ const unifiModule = {
reject(e); reject(e);
}); });
}); });
},
/**
* Returns a list with all UniFi Guests
*
* @param retry
* @return {Promise<unknown>}
*/
guests: (retry = true) => {
return new Promise((resolve, reject) => {
startSession().then(() => {
controller.getGuests().then((guests) => {
log.info(`[UniFi] Found ${guests.length} guest(s)`);
resolve(guests);
}).catch((e) => {
log.error('[UniFi] Error while getting guests!');
log.debug(e);
// Check if token expired, if true attempt login then try again
if (e.response) {
if(e.response.status === 401 && retry) {
log.info('[UniFi] Attempting re-authentication & retry...');
controller = null;
unifiModule.guests(false).then((e) => {
resolve(e);
}).catch((e) => {
reject(e);
});
} else {
// Something else went wrong lets clear the current controller so a user can retry
log.error(`[UniFi] Unexpected ${JSON.stringify({status: e.response.status, retry})} cleanup controller...`);
controller = null;
reject('[UniFi] Error while getting guests!');
}
} else {
// Something else went wrong lets clear the current controller so a user can retry
log.error('[UniFi] Unexpected cleanup controller...');
controller = null;
reject('[UniFi] Error while getting guests!');
}
});
}).catch((e) => {
reject(e);
});
});
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -38,6 +38,12 @@
"type": "image/png", "type": "image/png",
"form_factor": "narrow" "form_factor": "narrow"
}, },
{
"src": "./images/screenshots/mobile_screenshot_4.png",
"sizes": "413x877",
"type": "image/png",
"form_factor": "narrow"
},
{ {
"src": "./images/screenshots/desktop_screenshot_1.png", "src": "./images/screenshots/desktop_screenshot_1.png",
"sizes": "1280x720", "sizes": "1280x720",
@@ -55,6 +61,12 @@
"sizes": "1280x720", "sizes": "1280x720",
"type": "image/png", "type": "image/png",
"form_factor": "wide" "form_factor": "wide"
},
{
"src": "./images/screenshots/desktop_screenshot_4.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
} }
], ],
"start_url": "/", "start_url": "/",

View File

@@ -376,12 +376,21 @@ if(webService) {
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`); res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
}); });
if(vouchers) { log.info('[Cache] Requesting UniFi Guests...');
const guests = await unifi.guests().catch((e) => {
log.error('[Cache] Error requesting guests!');
res.cookie('flashMessage', JSON.stringify({type: 'error', message: e}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
});
if(vouchers && guests) {
cache.vouchers = vouchers; cache.vouchers = vouchers;
cache.guests = guests;
cache.updated = new Date().getTime(); cache.updated = new Date().getTime();
log.info(`[Cache] Saved ${vouchers.length} voucher(s)`); log.info(`[Cache] Saved ${vouchers.length} voucher(s)`);
log.info(`[Cache] Saved ${guests.length} guest(s)`);
res.cookie('flashMessage', JSON.stringify({type: 'info', message: 'Synced Vouchers!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`); res.cookie('flashMessage', JSON.stringify({type: 'info', message: 'Synced Vouchers & Guests!'}), {httpOnly: true, expires: new Date(Date.now() + 24 * 60 * 60 * 1000)}).redirect(302, `${req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''}/vouchers`);
} }
return; return;
@@ -401,6 +410,29 @@ if(webService) {
updated: cache.updated updated: cache.updated
}); });
}); });
app.get('/voucher/:id', [authorization.web], async (req, res) => {
const voucher = cache.vouchers.find((e) => {
return e._id === req.params.id;
});
const guests = cache.guests.filter((e) => {
return e.voucher_id === req.params.id;
});
if(voucher) {
res.render('components/details', {
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : '',
timeConvert: time,
bytesConvert: bytes,
voucher,
guests
});
} else {
res.status(404);
res.render('404', {
baseUrl: req.headers['x-ingress-path'] ? req.headers['x-ingress-path'] : ''
});
}
});
} }
if(apiService) { if(apiService) {

View File

@@ -0,0 +1,154 @@
<div class="relative z-40" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
<div class="fixed inset-0 overflow-hidden">
<div class="absolute inset-0 overflow-hidden">
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16">
<div class="pointer-events-auto w-screen max-w-md">
<div class="flex h-full flex-col divide-y divide-black/5 dark:divide-white/25 bg-white dark:bg-gray-900 shadow-xl">
<div class="h-0 flex-1 overflow-y-auto">
<div class="flex flex-1 flex-col justify-between">
<div class="divide-y divide-black/5 dark:divide-white/25 px-4 sm:px-6">
<div class="space-y-6 pb-5 pt-6">
<div>
<h3 class="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Voucher Details
</h3>
<p class="mt-1 text-sm leading-6 text-gray-600 dark:text-gray-400">
Voucher Details and Configuration.
</p>
</div>
<dl>
<div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Code</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.code.slice(0, 5) %>-<%= voucher.code.slice(5) %></dd>
</div>
<div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Status</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0">
<% if(voucher.used > 0) { %>
<div class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:text-yellow-400 dark:bg-yellow-400/10 dark:ring-yellow-400/20">
In Use
</div>
<% } else { %>
<div class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-green-50 text-green-700 ring-green-600/20 dark:text-green-400 dark:bg-green-400/10 dark:ring-green-400/20">
Available
</div>
<% } %>
</dd>
</div>
<div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Type</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.quota === 0 ? 'Multi-use' : 'Single-use' %></dd>
</div>
<div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Duration</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= timeConvert(voucher.duration) %></dd>
</div>
<div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Data Limit</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.qos_usage_quota ? bytesConvert(voucher.qos_usage_quota, 2) : 'Unlimited' %></dd>
</div>
<div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Download Limit</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.qos_rate_max_down ? bytesConvert(voucher.qos_rate_max_down, 1, true) : 'Unlimited' %></dd>
</div>
<div class="py-2 grid grid-cols-3 gap-4 px-0">
<dt class="text-sm font-medium leading-6 text-gray-900 dark:text-white">Upload Limit</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400 col-span-2 mt-0"><%= voucher.qos_rate_max_up ? bytesConvert(voucher.qos_rate_max_up, 1, true) : 'Unlimited' %></dd>
</div>
</dl>
</div>
<div class="space-y-6 pb-5 pt-6">
<div>
<h3 class="text-base font-semibold leading-7 text-gray-900 dark:text-white">
Guest Details
</h3>
<p class="mt-1 text-sm leading-6 text-gray-600 dark:text-gray-400">
Guest Details for Voucher.
</p>
</div>
<% if(guests.length < 1) { %>
<div>
<div class="relative block w-full rounded-lg border-2 border-dashed border-gray-300 p-7 text-center">
<svg class="mx-auto h-12 w-12 text-gray-600 dark:text-gray-400" stroke="currentColor" stroke-width="1.5" fill="none" viewBox="0 0 24 24" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />
</svg>
<span class="mt-2 block text-sm font-semibold text-gray-900 dark:text-white">
No Guests Connected
</span>
</div>
</div>
<% } else { %>
<% guests.forEach((guest) => { %>
<div class="lg:col-start-3 lg:row-end-1">
<div class="rounded-lg bg-gray-50 dark:bg-gray-800 shadow-sm ring-1 ring-gray-900/5 dark:ring-gray-50/25">
<dl class="flex flex-wrap">
<div class="flex-auto pl-6 pt-6">
<dt class="text-sm font-semibold leading-6 text-gray-900 dark:text-white"><%= guest.mac %></dt>
<dd class="mt-1 text-base font-semibold leading-6 text-gray-900 dark:text-white"><%= guest.hostname || guest.mac %></dd>
</div>
<div class="flex-none self-end px-6 pt-4">
<dt class="sr-only">Returning User</dt>
<% if(guest.is_returning) { %>
<dd class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-yellow-50 text-yellow-800 ring-yellow-600/20 dark:text-yellow-400 dark:bg-yellow-400/10 dark:ring-yellow-400/20">
Returning
</dd>
<% } else { %>
<dd class="rounded-full w-fit py-1 px-2 text-xs font-medium ring-1 ring-inset bg-green-50 text-green-700 ring-green-600/20 dark:text-green-400 dark:bg-green-400/10 dark:ring-green-400/20">
New
</dd>
<% } %>
</div>
<div class="mt-6 flex w-full flex-none gap-x-4 border-t border-black/5 dark:border-white/25 px-6 pt-6">
<dt class="flex-none">
<span class="sr-only">Expiration</span>
<svg class="h-6 w-5 text-gray-900 dark:text-white" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M5.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H6a.75.75 0 01-.75-.75V12zM6 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H6zM7.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H8a.75.75 0 01-.75-.75V12zM8 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H8zM9.25 10a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H10a.75.75 0 01-.75-.75V10zM10 11.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V12a.75.75 0 00-.75-.75H10zM9.25 14a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H10a.75.75 0 01-.75-.75V14zM12 9.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V10a.75.75 0 00-.75-.75H12zM11.25 12a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H12a.75.75 0 01-.75-.75V12zM12 13.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V14a.75.75 0 00-.75-.75H12zM13.25 10a.75.75 0 01.75-.75h.01a.75.75 0 01.75.75v.01a.75.75 0 01-.75.75H14a.75.75 0 01-.75-.75V10zM14 11.25a.75.75 0 00-.75.75v.01c0 .414.336.75.75.75h.01a.75.75 0 00.75-.75V12a.75.75 0 00-.75-.75H14z" />
<path fill-rule="evenodd" d="M5.75 2a.75.75 0 01.75.75V4h7V2.75a.75.75 0 011.5 0V4h.25A2.75 2.75 0 0118 6.75v8.5A2.75 2.75 0 0115.25 18H4.75A2.75 2.75 0 012 15.25v-8.5A2.75 2.75 0 014.75 4H5V2.75A.75.75 0 015.75 2zm-1 5.5c-.69 0-1.25.56-1.25 1.25v6.5c0 .69.56 1.25 1.25 1.25h10.5c.69 0 1.25-.56 1.25-1.25v-6.5c0-.69-.56-1.25-1.25-1.25H4.75z" clip-rule="evenodd" />
</svg>
</dt>
<dd class="text-sm font-medium leading-6 text-gray-600 dark:text-gray-400">
<%= new Intl.DateTimeFormat('en-US', {dateStyle: 'short', timeStyle: 'short', hour12: false}).format(new Date(guest.end * 1000)) %>
</dd>
</div>
<div class="mt-4 flex w-full flex-none gap-x-4 px-6">
<dt class="flex-none">
<span class="sr-only">Downloaded</span>
<svg class="h-6 w-5 text-gray-900 dark:text-white" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm.75-11.25a.75.75 0 0 0-1.5 0v4.59L7.3 9.24a.75.75 0 0 0-1.1 1.02l3.25 3.5a.75.75 0 0 0 1.1 0l3.25-3.5a.75.75 0 1 0-1.1-1.02l-1.95 2.1V6.75Z" clip-rule="evenodd" />
</svg>
</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400">
<%= bytesConvert(guest.tx_bytes) %>
</dd>
</div>
<div class="mt-4 mb-6 flex w-full flex-none gap-x-4 px-6">
<dt class="flex-none">
<span class="sr-only">Uploaded</span>
<svg class="h-6 w-5 text-gray-900 dark:text-white" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm-.75-4.75a.75.75 0 0 0 1.5 0V8.66l1.95 2.1a.75.75 0 1 0 1.1-1.02l-3.25-3.5a.75.75 0 0 0-1.1 0L6.2 9.74a.75.75 0 1 0 1.1 1.02l1.95-2.1v4.59Z" clip-rule="evenodd" />
</svg>
</dt>
<dd class="text-sm leading-6 text-gray-600 dark:text-gray-400">
<%= bytesConvert(guest.rx_bytes) %>
</dd>
</div>
</dl>
</div>
</div>
<% }); %>
<% } %>
</div>
</div>
</div>
</div>
<div class="flex flex-shrink-0 justify-end px-4 py-4">
<button id="close" type="button" class="rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50">Close</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -132,8 +132,8 @@
<ul role="list" class="divide-y divide-black/5 dark:divide-white/5"> <ul role="list" class="divide-y divide-black/5 dark:divide-white/5">
<% vouchers.forEach((voucher) => { %> <% vouchers.forEach((voucher) => { %>
<li class="relative flex items-center space-x-4 px-4 py-4 sm:px-6 lg:px-8"> <li class="relative flex items-center space-x-4 px-4 py-4 sm:px-6 lg:px-8 hover:bg-gray-200 dark:hover:bg-gray-800 cursor-pointer">
<div class="min-w-0 flex-auto"> <div class="voucher min-w-0 flex-auto" data-id="<%= voucher._id %>">
<div class="flex items-center gap-x-3"> <div class="flex items-center gap-x-3">
<h2 class="min-w-0 text-sm font-semibold leading-6 text-gray-900 dark:text-white"> <h2 class="min-w-0 text-sm font-semibold leading-6 text-gray-900 dark:text-white">
<div class="flex gap-x-2"> <div class="flex gap-x-2">
@@ -205,6 +205,8 @@
</main> </main>
</div> </div>
<div id="detail-dialog"></div>
<div id="create-dialog" class="hidden relative z-40" role="dialog" aria-modal="true"> <div id="create-dialog" class="hidden relative z-40" role="dialog" aria-modal="true">
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div> <div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
@@ -362,6 +364,7 @@
</script> </script>
<script type="application/javascript"> <script type="application/javascript">
const createDialog = document.querySelector('#create-dialog'); const createDialog = document.querySelector('#create-dialog');
const detailDialog = document.querySelector('#detail-dialog');
const createForum = document.querySelector("#voucher-forum"); const createForum = document.querySelector("#voucher-forum");
const voucherTypeField = document.querySelector('#voucher-type'); const voucherTypeField = document.querySelector('#voucher-type');
const customVoucherFields = document.querySelectorAll('.custom-voucher-field'); const customVoucherFields = document.querySelectorAll('.custom-voucher-field');
@@ -375,6 +378,12 @@
const spinnerRemove = document.querySelector("#spinner-remove"); const spinnerRemove = document.querySelector("#spinner-remove");
const spinnerList = document.querySelector("#spinner-list"); const spinnerList = document.querySelector("#spinner-list");
const copyNotification = document.querySelector("#copy-notification"); const copyNotification = document.querySelector("#copy-notification");
const vouchers = document.querySelectorAll('.voucher');
const clearDetailDialog = () => {
document.querySelector('#close').removeEventListener('click', clearDetailDialog);
detailDialog.innerHTML = '';
};
createButtonHeader.addEventListener('click', () => { createButtonHeader.addEventListener('click', () => {
createDialog.classList.remove('hidden'); createDialog.classList.remove('hidden');
@@ -423,6 +432,18 @@
}); });
} }
}); });
vouchers.forEach((el) => {
el.addEventListener('click', async () => {
const htmlRes = await fetch(`<%= baseUrl %>/voucher/${el.dataset.id}`);
if(htmlRes.status === 200 && !htmlRes.redirected) {
detailDialog.innerHTML = await htmlRes.text();
document.querySelector('#close').addEventListener('click', clearDetailDialog);
} else {
window.location.reload();
}
});
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -30,6 +30,18 @@ module.exports = {
log.info(`[Cache] Saved ${vouchers.length} voucher(s)`); log.info(`[Cache] Saved ${vouchers.length} voucher(s)`);
} }
log.info('[Cache] Requesting UniFi Guests...');
const guests = await unifi.guests().catch(() => {
log.error('[Cache] Error requesting guests!');
});
if(guests) {
cache.guests = guests;
cache.updated = new Date().getTime();
log.info(`[Cache] Saved ${guests.length} guest(s)`);
}
resolve(); resolve();
}); });
} }