Skip to content

Commit

Permalink
feat(desktop-notifications): new service worker module, bugfixes and …
Browse files Browse the repository at this point in the history
…refactorings

Encapsulated service worker logic into an own JS module. There are numerous bugfixes in regard to the display of notifications in the major browsers.
Important: The notifications currently won't work in WebKit based browsers like Safari, as the notification toggle logic which requires a user interaction has not been added yet.
  • Loading branch information
ncosta-ic committed Feb 1, 2024
1 parent b37b3c4 commit 9bda33d
Show file tree
Hide file tree
Showing 23 changed files with 1,940 additions and 293 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
!.git*
!.github
!.phpcs.xml
/dist/serviceworker/node_modules/
94 changes: 68 additions & 26 deletions application/controllers/DaemonController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Icinga\Application\Icinga;
use ipl\Web\Compat\CompatController;
use ipl\Web\Compat\ViewRenderer;

final class DaemonController extends CompatController
{
Expand All @@ -12,44 +13,85 @@ public function init(): void
/**
* override init function and disable Zend rendering as this controller provides no graphical output
*/
$this->_helper->viewRenderer->setNoRender(true);
$this->_helper->layout()->disableLayout();
/** @var ViewRenderer $viewRenderer */
$viewRenderer = $this->getHelper('viewRenderer');
$viewRenderer->setNoRender(true);
/** @var \Zend_Layout $layout */
$layout = $this->getHelper('layout');
$layout->disableLayout();
}

public function scriptAction(): void
{
$mime = '';
switch ($this->_getParam('extension', 'undefined')) {
case 'undefined':
$this->httpNotFound("File extension is missing.");
case '.js':
$mime = 'application/javascript';
break;
case '.js.map':
$mime = 'application/json';
break;
}
$root = Icinga::app()
->getModuleManager()
->getModule('notifications')
->getBaseDir() . '/public/js';

$filePath = realpath($root . DIRECTORY_SEPARATOR . 'icinga-notifications-worker.js');
if ($filePath === false) {
$this->httpNotFound("'icinga-notifications-worker.js' does not exist");
}

$fileStat = stat($filePath);
$eTag = sprintf(
'%x-%x-%x',
$fileStat['ino'],
$fileStat['size'],
(float) str_pad($fileStat['mtime'], 16, '0')
$filePath = realpath(
$root . DIRECTORY_SEPARATOR . 'icinga-notifications-' . $this->_getParam(
'file',
'undefined'
) . $this->_getParam('extension', 'undefined')
);
if ($filePath === false) {
if ($this->_getParam('file') === null) {
$this->httpNotFound("No file name submitted");
}
$this->httpNotFound(
"'icinga-notifications-"
. $this->_getParam('file')
. $this->_getParam('extension')
. " does not exist"
);
} else {
$fileStat = stat($filePath);
$eTag = '';
if ($fileStat) {
$eTag = sprintf(
'%x-%x-%x',
$fileStat['ino'],
$fileStat['size'],
(float) str_pad((string) ($fileStat['mtime']), 16, '0')
);

$this->getResponse()->setHeader(
'Cache-Control',
'public, max-age=1814400, stale-while-revalidate=604800',
true
);
$this->getResponse()->setHeader(
'Cache-Control',
'public, max-age=1814400, stale-while-revalidate=604800',
true
);

if ($this->getRequest()->getServer('HTTP_IF_NONE_MATCH') === $eTag) {
$this->getResponse()->setHttpResponseCode(304);
} else {
$this->getResponse()
->setHeader('ETag', $eTag)
->setHeader('Content-Type', 'text/javascript', true)
->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $fileStat['mtime']) . ' GMT')
->setBody(file_get_contents($filePath));
if ($this->getRequest()->getServer('HTTP_IF_NONE_MATCH') === $eTag) {
$this->getResponse()->setHttpResponseCode(304);
} else {
$this->getResponse()
->setHeader('ETag', $eTag)
->setHeader('Content-Type', $mime, true)
->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $fileStat['mtime']) . ' GMT');
$file = file_get_contents($filePath);
if ($file) {
$this->getResponse()->setBody($file);
}
}
} else {
$this->httpNotFound(
"'icinga-notifications-"
. $this->_getParam('file')
. $this->_getParam('extension')
. " could not be read"
);
}
}
}
}
35 changes: 35 additions & 0 deletions dist/serviceworker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
### Prerequisites
```shell
# 1. NODE VERSION MANAGER
# make sure to install the latest (lts) version of NodeJS
# the easiest way of doing so is by using the n manager (https://github.com/tj/n)
# and its installer (https://github.com/mklement0/n-install)
curl -L https://bit.ly/n-install | bash
n install lts
# after a directory change to .../icinga-notifications-web/dist/serviceworker
npm update && npm install

# 2. PHPSTORM SETTINGS
# open the TypeScript configuration in PhpStorm:
# File | Settings | Languages & Frameworks | TypeScript
# or via link: jetbrains://PhpStorm/settings?name=Languages+%26+Frameworks--TypeScript
# set the node interpreter to these settings:
# Node interpreter: ~/n/bin/node (or the equivalent path of the 'n' manager installation)
# Options: --target ES6
```

### Build
```shell
# the generated files are placed under ./build/{*.js|*.js.map}

# development
npm run build
# production
npm run build-prod
```

### Serve local test server
```shell
# remove the -o if you don't want to open the default browser automatically
http-server ./example -p 9216 -c-1 -o
```
2 changes: 2 additions & 0 deletions dist/serviceworker/build/icinga-notifications-worker.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions dist/serviceworker/build/icinga-notifications-worker.js.map

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions dist/serviceworker/esbuild-prod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {build} from 'esbuild';

build({
entryPoints: ['source/icinga-notifications-worker.ts'],
outdir: 'build',
bundle: true,
sourcemap: true,
minify: true,
splitting: false,
format: 'iife',
define: { global: 'globalThis' },
target: ['chrome58', 'firefox57', 'safari11', 'edge16'],
globalName: 'icinga.notifications.worker',
legalComments: 'none',
logLevel: 'debug'
})
.catch((process) => process.exit(1));
17 changes: 17 additions & 0 deletions dist/serviceworker/esbuild.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {build} from 'esbuild';

build({
entryPoints: ['source/icinga-notifications-worker.ts'],
outdir: 'build',
bundle: true,
sourcemap: true,
minify: false,
splitting: false,
format: 'iife',
define: { global: 'globalThis' },
target: ['chrome58', 'firefox57', 'safari11', 'edge16'],
globalName: 'icinga.notifications.worker',
legalComments: 'none',
logLevel: 'debug'
})
.catch((process) => process.exit(1));
1 change: 1 addition & 0 deletions dist/serviceworker/example/icinga-notifications-worker.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 48 additions & 0 deletions dist/serviceworker/example/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>IcingaNotificationsWorker :: Test</title>
</head>
<body>
<main>
<p>Test123</p>
</main>
<script>
try {
navigator.serviceWorker.register(
'/icinga-notifications-worker.js',
{
scope: '/',
type: 'classic'
}
).then((registration) => {
if (registration.installing) {
console.log("Service worker is installing.");
} else if (registration.waiting) {
console.log("Service worker has been installed and is waiting to be run.");
} else if (registration.active) {
console.log("Service worker has been activated.");
setTimeout(() => {
let es = new EventSource('/icingaweb2/notifications/daemon');
});
}

if (navigator.serviceWorker.controller === null) {
/**
* hard refresh detected. This causes the browser to not forward fetch requests to
* service workers. Reloading the site fixes this.
*/
setTimeout(() => {
console.log("Hard refresh detected. Reloading page to fix the service workers.");
location.reload();
}, 1000);
return;
}
});
} catch (error) {
console.error(`Service worker failed to register: ${error}`);
}
</script>
</body>
</html>
Loading

0 comments on commit 9bda33d

Please sign in to comment.