Initial commit

This commit is contained in:
William Brawner 2022-02-04 18:08:07 -07:00
commit 3a94e603d3
31 changed files with 4528 additions and 0 deletions

22
.github/workflows/docker-image.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: Publish Docker image
on:
push:
branches: main
tags:
- '*'
jobs:
push_to_registry:
name: Push Docker image to GitHub Packages
runs-on: ubuntu-latest
steps:
- name: Check out the repo
uses: actions/checkout@v2
- name: Push to GitHub Packages
uses: docker/build-push-action@v1
with:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}/${{ github.event.repository.name }}
registry: docker.pkg.github.com
tag_with_ref: true

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
.env
.vscode/
dist/
node_modules/

6
Dockerfile Normal file
View file

@ -0,0 +1,6 @@
FROM node:lts
COPY . /app
WORKDIR /app
RUN ls
RUN npm install
ENTRYPOINT npm start

50
client/index.html Normal file
View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<title>Pi-helper</title>
<meta charset="utf-8">
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="application-name" content="Twigs">
<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" href="/static/icons/icon-192x192.png">
<link rel="stylesheet" href="/static/css/style.css" />
<script type="text/javascript" src="/static/js/index.js"></script>
</head>
<body onload="monitorChanges()">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 404 404" class="spin" id="logo">
<g id="group" transform="translate(66.72238 66.72238)">
<path id="path"
d="M 135.467 61.19 C 94.537 61.19 61.19 94.537 61.19 135.467 C 61.19 147.777 64.21 159.399 69.542 169.636 C 64.167 175.795 61.2 183.696 61.191 191.871 C 61.191 200.851 64.762 209.472 71.112 215.822 C 77.462 222.172 86.083 225.743 95.063 225.743 C 100.983 225.736 106.799 224.178 111.929 221.224 C 117.059 218.269 121.326 214.021 124.303 208.904 C 124.303 208.904 124.304 208.903 124.304 208.903 C 127.946 209.455 131.674 209.743 135.467 209.743 C 176.397 209.743 209.744 176.397 209.744 135.467 C 209.744 94.537 176.397 61.19 135.467 61.19 Z M 135.467 76.748 C 167.99 76.748 194.192 102.943 194.192 135.466 C 194.192 167.989 167.99 194.191 135.467 194.191 C 133.225 194.191 131.014 194.062 128.839 193.819 C 128.839 193.817 128.839 193.814 128.839 193.812 C 128.89 193.165 128.922 192.517 128.936 191.869 C 128.935 182.889 125.364 174.268 119.014 167.919 C 112.664 161.569 104.043 157.999 95.063 157.999 C 90.692 158.007 86.364 158.86 82.318 160.512 C 82.318 160.512 82.318 160.511 82.317 160.511 C 82.315 160.512 82.313 160.513 82.311 160.514 C 78.743 152.923 76.747 144.437 76.747 135.467 C 76.747 102.944 102.942 76.749 135.465 76.749 Z"
fill="#000000" stroke="#000000" stroke-width="1.72941" />
</g>
</svg>
<div class="enabled" id="enabled">
<p>Status: <span class="status">Enabled</span></p>
<button onclick="disable(30)">Disable for 30 seconds</button>
<button onclick="disable(60)">Disable for 1 minute</button>
<button onclick="disable(300)">Disable for 5 minutes</button>
<button onclick="showEnable(false, true)">Disable for custom time</button>
<button onclick="disable()">Disable Permanently</button>
</div>
<div class="disabled" id="disabled">
<p>Status: <span class="status">Disabled</span><span class="status" id="duration"></span></p>
<button onclick="enable()">Enable</button>
</div>
<div class="enabled custom" id="disable-custom">
<p>Disable for a custom time</p>
<input type="number" id="disable-duration" value="10" />
<div class="units">
<button id="seconds" class="unit selected" onclick="setUnit('seconds')">Seconds</button>
<button id="minutes" class="unit" onclick="setUnit('minutes')">Minutes</button>
<button id="hours" class="unit" onclick="setUnit('hours')">Hours</button>
<input type="hidden" name="unit" value="seconds" id="unit" />
</div>
<button onclick="disableCustom()">Disable</button>
<button class="outline" onclick="showEnable(true)">Cancel</button>
</div>
</body>
</html>

99
client/manifest.json Normal file
View file

@ -0,0 +1,99 @@
{
"name": "Pi-helper",
"short_name": "Pi-helper",
"theme_color": "#000000",
"background_color": "#000000",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "static/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "static/icons/icon-maskable-72x72.png",
"sizes": "72x72",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "static/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "static/icons/icon-maskable-96x96.png",
"sizes": "96x96",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "static/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "static/icons/icon-maskable-128x128.png",
"sizes": "128x128",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "static/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "static/icons/icon-maskable-144x144.png",
"sizes": "144x144",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "static/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "static/icons/icon-maskable-152x152.png",
"sizes": "152x152",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "static/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "static/icons/icon-maskable-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "static/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "static/icons/icon-maskable-384x384.png",
"sizes": "384x384",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "static/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "static/icons/icon-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}

18
client/package.json Normal file
View file

@ -0,0 +1,18 @@
{
"name": "client",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"prebuild": "copyfiles -Ve src/** index.html manifest.json **/*.css **/*.png ../dist/public",
"build": "tsc --project ./",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"devDependencies": {
"copyfiles": "^2.4.1",
"eslint": "^8.8.0",
"typescript": "^4.5.5"
}
}

187
client/src/index.ts Normal file
View file

@ -0,0 +1,187 @@
let socket: WebSocket | null = null;
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/static/js/sw.js')
.then(function (registration) {
console.log('Registration successful, scope is:', registration.scope);
})
.catch(function (error) {
console.log('Service worker registration failed, error:', error);
});
}
function monitorChanges() {
const protocol = location.protocol === 'https:' ? 'wss' : 'ws';
socket = new WebSocket(`${protocol}://${location.host}/`)
socket.onopen = (e) => {
console.log('socket opened', e)
}
socket.onmessage = (e) => {
const data = JSON.parse(e.data)
console.log('message received', data)
switch (data.status) {
case 'enabled':
if (durationInterval) {
clearInterval(durationInterval)
}
animateLogo(false)
showDisable(false)
showEnable(true)
break
case 'disabled':
animateLogo(false)
showEnable(false, false)
showDisable(true, data.until)
break
default:
console.error('Unhandled status', data)
}
}
socket.onclose = (e) => {
console.log('socket closed', e)
setTimeout(monitorChanges, 1000)
}
socket.onerror = (e) => {
console.error('socket error', e)
}
}
function enable() {
if (!socket) return;
showDisable(false)
animateLogo(true)
const command = { action: 'enable' };
socket.send(JSON.stringify(command))
}
function disable(duration: number) {
if (!socket) return;
showEnable(false)
animateLogo(true)
const command = { action: 'disable', duration: duration };
socket.send(JSON.stringify(command))
}
function setUnit(unit: string) {
const unitInput = document.getElementById('unit') as HTMLInputElement
const units = document.getElementsByClassName('unit')
for (let i = 0; i < units.length; i++) {
if (units[i].id === unit) {
units[i].classList.add('selected')
} else {
units[i].classList.remove('selected')
}
}
unitInput.value = unit
}
function disableCustom() {
showEnable(false, false)
}
function animateLogo(animate: boolean) {
const logo = document.getElementById('logo') as HTMLElement
if (animate) {
logo.classList.add('spin')
} else {
logo.classList.remove('spin')
}
}
function showEnable(show: boolean, showCustom?: boolean) {
const enableDiv = document.getElementById('enabled') as HTMLElement
const disableCustom = document.getElementById('disable-custom') as HTMLElement
if (show) {
if (disableCustom.style.opacity === '1') {
disableCustom.style.opacity = '0'
setTimeout(() => {
disableCustom.style.maxHeight = '0'
enableDiv.style.maxHeight = '100vh'
}, 250)
setTimeout(() => {
enableDiv.style.opacity = '1'
}, 500)
} else {
enableDiv.style.maxHeight = '100vh'
setTimeout(() => {
enableDiv.style.opacity = '1'
}, 250)
}
} else {
enableDiv.style.opacity = '0'
setTimeout(() => {
enableDiv.style.maxHeight = '0'
}, 250)
if (showCustom) {
setTimeout(() => {
disableCustom.style.maxHeight = '100vh'
}, 250)
setTimeout(() => {
disableCustom.style.opacity = '1'
}, 500)
}
}
}
let durationInterval: any;
function showDisable(show: boolean, timestamp?: number) {
const disableDiv = document.getElementById('disabled') as HTMLElement
if (show) {
disableDiv.style.maxHeight = '100vh'
} else {
disableDiv.style.opacity = '0'
}
setTimeout(() => {
if (show) {
disableDiv.style.opacity = '1'
} else {
disableDiv.style.maxHeight = '0'
}
}, 250)
const duration = document.getElementById('duration') as HTMLElement
if (!timestamp) {
duration.innerText = ''
return
}
const until = new Date(timestamp)
function updateDuration() {
const now = new Date();
const difference = until.getTime() - now.getTime();
if (durationInterval && difference <= 0) {
duration.innerText = ''
clearInterval(durationInterval)
if (socket) {
const command = { action: 'status' };
socket.send(JSON.stringify(command))
}
return
}
let seconds = Math.ceil(difference / 1000)
console.log(`${until.getTime()} - ${now.getTime()} = ${seconds} seconds`)
let hours = 0
let minutes = 0
let durationText: string = '';
if (seconds >= 3600) {
hours = Math.floor(seconds / 3600)
seconds -= hours * 3600
}
console.log(`hours: ${hours} seconds: ${seconds}`)
if (seconds >= 60) {
minutes = Math.floor(seconds / 60)
seconds -= minutes * 60
}
console.log(`minutes: ${minutes} seconds: ${seconds}`)
if (hours > 0) {
durationText += `${hours.toString().padStart(2, '0')}:`
}
durationText += `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
duration.innerText = ` (${durationText})`
}
durationInterval = setInterval(updateDuration, 1000)
}

49
client/src/sw.ts Normal file
View file

@ -0,0 +1,49 @@
const filesToCache = [
'/',
'/index.html',
'/static/css/style.css',
'/static/icons/icon-128x128.png',
'/static/icons/icon-144x144.png',
'/static/icons/icon-152x152.png',
'/static/icons/icon-192x192.png',
'/static/icons/icon-384x384.png',
'/static/icons/icon-512x512.png',
'/static/icons/icon-72x72.png',
'/static/icons/icon-96x96.png',
'/static/icons/icon-maskable-128x128.png',
'/static/icons/icon-maskable-144x144.png',
'/static/icons/icon-maskable-152x152.png',
'/static/icons/icon-maskable-192x192.png',
'/static/icons/icon-maskable-384x384.png',
'/static/icons/icon-maskable-512x512.png',
'/static/icons/icon-maskable-72x72.png',
'/static/icons/icon-maskable-96x96.png'
];
const staticCacheName = 'pages-cache-v1';
// TODO: Import this properly
type InstallEvent = any;
self.addEventListener('install', (event: InstallEvent) => {
console.log('Attempting to install service worker and cache static assets');
event.waitUntil(
caches.open(staticCacheName)
.then(cache => {
return cache.addAll(filesToCache);
})
);
});
self.addEventListener('fetch', (event: InstallEvent) => {
event.respondWith(
caches.match(event.request).then((response) => {
caches.open(staticCacheName).then((cache) => {
if (event.request.url.match(/^https?/)) {
cache.add(event.request.url);
}
})
return response || fetch(event.request);
})
);
});

163
client/static/css/style.css Normal file
View file

@ -0,0 +1,163 @@
:root {
--color-red-light: #f60d1a;
--color-red-dark: #96060c;
--color-green-light: #29fc2e;
--color-green-dark: #22b225;
--color-grey-light: #cbcbcb;
--color-grey-medium: #cbcbcb;
--color-grey-dark: #333333;
}
@media (prefers-color-scheme: light) {
:root {
--color-background: #FFFFFF;
--color-foreground: #000000;
--color-foreground-enabled: var(--color-green-dark);
--color-foreground-disabled: var(--color-red-light);
--color-background-enabled: var(--color-green-dark);
--color-background-disabled: var(--color-red-light);
--color-onbackground-enabled: #FFFFFF;
--color-onbackground-disabled: #FFFFFF;
}
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: #000000;
--color-foreground: #F1F1F1;
--color-foreground-enabled: var(--color-green-dark);
--color-foreground-disabled: var(--color-red-dark);
--color-background-enabled: var(--color-green-dark);
--color-background-disabled: var(--color-red-dark);
--color-onbackground-enabled: white;
--color-onbackground-disabled: white;
}
}
* {
transition: all 0.25s ease;
}
html, body {
margin: auto;
padding: 0;
font-family: sans-serif;
text-align: center;
height: 100vh;
background-color: var(--color-background);
color: var(--color-foreground);
}
body, div {
display: flex;
flex-direction: column;
max-width: 400px;
align-items: center;
justify-content: center;
max-height: 100vh;
}
body {
padding: 10px;
}
div, div > * {
width: 100%;
}
svg {
height: 10em;
width: 10em;
}
path {
fill: var(--color-foreground);
}
button {
border-radius: 5px;
border: 0;
padding: 10px;
cursor: pointer;
}
button, p {
margin: 5px 0;
font-size: 1em;
}
#enabled, #disabled, #disable-custom {
max-height: 0;
opacity: 0;
overflow: hidden;
}
.enabled .status {
color: var(--color-foreground-enabled);
}
.enabled button {
background-color: var(--color-background-disabled);
color: var(--color-onbackground-disabled);
}
.enabled button.outline {
background-color: var(--color-background);
color: var(--color-foreground);
border: 1px solid var(--color-foreground-disabled);
}
.disabled .status {
color: var(--color-red-light);
}
.disabled button {
background-color: var(--color-background-enabled);
color: var(--color-onbackground-enabled);
}
.disabled button.outline {
background-color: var(--color-background);
color: var(--color-foreground);
border: 1px solid var(--color-foreground-enabled);
}
.spin {
animation-name: spin;
animation-duration: 1s;
animation-timing-function: linear;
animation-iteration-count: infinite;
}
@keyframes spin {
from { transform: rotate(0deg) }
to { transform: rotate(360deg) }
}
.custom * {
box-sizing: border-box;
}
.units {
flex-direction: row;
}
.enabled button.unit {
background: var(--color-background);
color: var(--color-foreground);
}
.enabled button.unit.selected {
background: var(--color-grey-medium);
color: var(--color-grey-dark);
}
input {
background-color: var(--color-background);
border-radius: 5px;
border: 1px solid var(--color-foreground-disabled);
color: var(--color-foreground);
font-size: 1em;
margin: 5px 0;
padding: 10px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

101
client/tsconfig.json Normal file
View file

@ -0,0 +1,101 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "resolveJsonModule": true, /* Enable importing .json files */
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "../dist/public/static/js", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

3558
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

19
package.json Normal file
View file

@ -0,0 +1,19 @@
{
"name": "pihelper",
"version": "1.0.0",
"description": "A PWA to manage the status of your Pi-hole",
"scripts": {
"build": "npm run build --workspaces",
"prestart": "npm run -w client build",
"start": "npm run -w server start"
},
"author": "William Brawner <me@wbrawner.com>",
"license": "MIT",
"workspaces": [
"client",
"server"
],
"devDependencies": {
"concurrently": "^7.0.0"
}
}

130
server/index.ts Normal file
View file

@ -0,0 +1,130 @@
import express from 'express'
import ws from 'ws'
import http from 'http'
const port = process.env.PIHELPER_PORT || 3000;
const pihole = process.env.PIHELPER_PIHOLE_HOST || 'http://pi.hole';
const apiKey: string = process.env.PIHELPER_API_KEY || '';
if (apiKey === '') {
console.error("PIHELPER_API_KEY not set, aborting")
process.exit(1)
}
const app = express();
const wss = new ws.Server({ noServer: true });
const clients: Array<ws> = []
wss.on('connection', socket => {
clients.push(socket)
socket.on('close', () => {
const index = clients.indexOf(socket);
if (index > -1) {
delete clients[index];
}
})
socket.on('message', message => {
const command = JSON.parse(message.toString())
switch (command.action) {
case 'status':
status(status => {
clients.forEach(client => {
client.send(status)
})
})
break
case 'enable':
enable(status => {
clients.forEach(client => {
client.send(status)
})
})
break
case 'disable':
disable(command.duration, status => {
clients.forEach(client => {
client.send(status)
})
})
break
default:
socket.send(`{"error": "Invalid command sent: '${command.action}'"}`)
}
})
status(status => {
clients.forEach(client => {
client.send(status)
})
})
})
// TODO: Handle errors better than this
function get(url: URL, callback: (data: string) => void, error?: (error?: Error) => void) {
const req = http.request(url, res => {
res.on('data', data => {
callback(data.toString())
})
})
req.on('response', res => {
if (res.statusCode != 200 && error) {
error()
}
})
req.on('error', e => {
console.error(`Failed to send GET request to ${url}`, e)
if (error) {
error(e)
}
})
req.end()
}
function status(callback: (status: any) => void) {
let url = new URL(`${pihole}/admin/api.php`)
get(url, data => {
let dataObj = JSON.parse(data)
if (dataObj.status === 'disabled') {
url = new URL(`${pihole}/custom_disable_timer`)
get(url, timestamp => {
dataObj["until"] = Number.parseInt(timestamp)
callback(JSON.stringify(dataObj))
}, e => {
callback(data)
})
} else {
callback(data)
}
})
}
function enable(callback: (status: any) => void) {
let url = new URL(`${pihole}/admin/api.php`)
url.searchParams.append('enable', '')
url.searchParams.append('auth', apiKey)
get(url, callback)
}
function disable(duration: number, callback: (status: any) => void) {
let url = new URL(`${pihole}/admin/api.php`)
url.searchParams.append('auth', apiKey)
url.searchParams.append('disable', duration.toString())
get(url, (data) => {
url = new URL(`${pihole}/custom_disable_timer`)
get(url, (timestamp) => {
let dataObj = JSON.parse(data)
dataObj["until"] = Number.parseInt(timestamp)
callback(JSON.stringify(dataObj))
})
})
}
app.use(express.static('./public'))
const server = app.listen(port, () => {
console.info(`Pi-helper listening on port ${port}`)
})
server.on('upgrade', (req, socket, head) => {
wss.handleUpgrade(req, socket, head, socket => {
wss.emit('connection', socket, req)
})
})

21
server/package.json Normal file
View file

@ -0,0 +1,21 @@
{
"name": "server",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"build": "tsc --project ./",
"prestart": "npm run build",
"start": "cd ../dist/; node index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"devDependencies": {
"@types/express": "^4.17.13",
"@types/ws": "^8.2.2",
"express": "^4.17.2",
"typescript": "^4.5.5",
"ws": "^8.4.2"
}
}

101
server/tsconfig.json Normal file
View file

@ -0,0 +1,101 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "resolveJsonModule": true, /* Enable importing .json files */
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "../dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}