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.
20
README.md
@@ -4,7 +4,7 @@ A small UniFi Voucher Site for simple voucher creation
|
|||||||
|
|
||||||
[](https://hub.docker.com/r/glenndehaan/unifi-voucher-site)
|
[](https://hub.docker.com/r/glenndehaan/unifi-voucher-site)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
@@ -233,22 +233,28 @@ The application will automatically format the voucher for 80mm paper width, ensu
|
|||||||
## Screenshots
|
## Screenshots
|
||||||
|
|
||||||
### Login (Desktop)
|
### Login (Desktop)
|
||||||

|

|
||||||
|
|
||||||
### Vouchers Overview (Desktop)
|
### Vouchers Overview (Desktop)
|
||||||

|

|
||||||
|
|
||||||
### Create Voucher (Desktop)
|
### Create Voucher (Desktop)
|
||||||

|

|
||||||
|
|
||||||
|
### Voucher Details (Desktop)
|
||||||
|

|
||||||
|
|
||||||
### Login (Mobile)
|
### Login (Mobile)
|
||||||

|

|
||||||
|
|
||||||
### Vouchers Overview (Mobile)
|
### Vouchers Overview (Mobile)
|
||||||

|

|
||||||
|
|
||||||
### Create Voucher (Mobile)
|
### Create Voucher (Mobile)
|
||||||

|

|
||||||
|
|
||||||
|
### Voucher Details (Mobile)
|
||||||
|

|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 85 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 106 KiB |
BIN
public/images/screenshots/desktop_screenshot_4.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 62 KiB |
BIN
public/images/screenshots/mobile_screenshot_4.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
@@ -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": "/",
|
||||||
|
|||||||
36
server.js
@@ -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) {
|
||||||
|
|||||||
154
template/components/details.ejs
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||