From 884645ea2425aa903c9ba70f1e615d17ed0515ce Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 29 Mar 2026 14:21:54 -0400 Subject: [PATCH 1/3] #544 add notification log feature with merge fix --- angular-client/package-lock.json | 116 +++++++++--------- .../app/app-nav-bar/app-nav-bar.component.css | 27 ++++ .../app-nav-bar/app-nav-bar.component.html | 25 ++++ .../app/app-nav-bar/app-nav-bar.component.ts | 20 ++- angular-client/src/app/app-routing.module.ts | 8 +- .../src/app/context/app-context.component.ts | 4 +- .../notification-dropdown.component.css | 83 +++++++++++++ .../notification-dropdown.component.html | 50 ++++++++ .../notification-dropdown.component.ts | 24 ++++ .../notification-log-panel.component.css | 74 +++++++++++ .../notification-log-panel.component.html | 43 +++++++ .../notification-log-panel.component.ts | 16 +++ .../notification-log-page.component.css | 31 +++++ .../notification-log-page.component.html | 55 +++++++++ .../notification-log-page.component.ts | 17 +++ .../src/services/notification-log.service.ts | 43 +++++++ angular-client/src/services/socket.service.ts | 14 ++- angular-client/src/utils/types.utils.ts | 7 ++ 18 files changed, 593 insertions(+), 64 deletions(-) create mode 100644 angular-client/src/components/notification-dropdown/notification-dropdown.component.css create mode 100644 angular-client/src/components/notification-dropdown/notification-dropdown.component.html create mode 100644 angular-client/src/components/notification-dropdown/notification-dropdown.component.ts create mode 100644 angular-client/src/components/notification-log-panel/notification-log-panel.component.css create mode 100644 angular-client/src/components/notification-log-panel/notification-log-panel.component.html create mode 100644 angular-client/src/components/notification-log-panel/notification-log-panel.component.ts create mode 100644 angular-client/src/pages/notification-log-page/notification-log-page.component.css create mode 100644 angular-client/src/pages/notification-log-page/notification-log-page.component.html create mode 100644 angular-client/src/pages/notification-log-page/notification-log-page.component.ts create mode 100644 angular-client/src/services/notification-log.service.ts diff --git a/angular-client/package-lock.json b/angular-client/package-lock.json index 807fdfcf..19a5027e 100644 --- a/angular-client/package-lock.json +++ b/angular-client/package-lock.json @@ -458,6 +458,7 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.2.11.tgz", "integrity": "sha512-NR33bZVho7EgTc1fmCnmkwc2/U266n311Wfvk7VVtz+0Q9WliNdDLBon654V8IWSKvlqKXyU3W+fp0VjH/FvSw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -658,6 +659,7 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.11.tgz", "integrity": "sha512-G568yWIJlnsuS563WxvCofmxc1405+wRQvDGQ32+qWOblJScFkHgr4jeDkZGcyt/r8OudaW0H0/rNeg1dzdnIQ==", "license": "MIT", + "peer": true, "dependencies": { "parse5": "^7.1.2", "tslib": "^2.3.0" @@ -733,6 +735,7 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.2.11.tgz", "integrity": "sha512-/ZnF2Nfp6S6TAu3VlvUAIp4NVd81WE1Q95wuwSSuoEx2aSyXzI+1myyKWSYe/jYCyGuppmocjTciEh8mAInmOw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -749,6 +752,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.2.11.tgz", "integrity": "sha512-/ZGFAEO2TyqkaE4neR8lGL9I2QeO2sRVFqulQv7Bu8zKTPStjcsFCwNkp+TNX8Oq/1rLcY9XWAOsUk1//AZd8Q==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -761,6 +765,7 @@ "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.2.11.tgz", "integrity": "sha512-15aoOg+qj7Z3Uap1JKHMy51y12M09AOnseDBa0SYKidSx15XwZi8d01hv7sRaQJX/6Ie5cug9GiAbLKts6R33w==", "license": "MIT", + "peer": true, "dependencies": { "@babel/core": "7.26.9", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -834,6 +839,7 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.2.11.tgz", "integrity": "sha512-kmtJQB7B5F2V1JIzy1oBPS6WrRyedSYkuge+XoX1mCSFJDef8HRNd7GopnQ0Zaz0vOTGvCCkWvvaH/+7s2lmAQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -850,6 +856,7 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.2.11.tgz", "integrity": "sha512-ZH9ccuT6rTirNSbiMRtGRkRrj69a2/+BVaa/kEpUHjh41wDQXxhOlOfPZd/sfj04QiAzIpsYmVJrmoV7/LxPSw==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -885,6 +892,7 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.2.11.tgz", "integrity": "sha512-wAPJtgzmxBEpW31sa2eg9QssCHBZ52Zc9nm6azTflDlOAyfm9bzqec7y3wqy5sgVue/qID2gzHqmpS3Nx3o0xg==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -925,6 +933,7 @@ "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.11.tgz", "integrity": "sha512-nBwMwRgQ3s1c1CPItPnTJTf81NDOQHvK41r2MIJGHa3H9LONlcbY07q/9p49fqt/xn/dgoOmQTtJ22b/nbIJAQ==", "license": "MIT", + "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -966,6 +975,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -3386,6 +3396,7 @@ "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.3.2.tgz", "integrity": "sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==", "license": "MIT", + "peer": true, "dependencies": { "@inquirer/checkbox": "^4.1.2", "@inquirer/confirm": "^5.1.6", @@ -5093,8 +5104,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.34.8", @@ -5314,7 +5324,6 @@ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.draggable.js/-/svg.draggable.js-3.0.6.tgz", "integrity": "sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==", "license": "MIT", - "peer": true, "peerDependencies": { "@svgdotjs/svg.js": "^3.2.4" } @@ -5324,7 +5333,6 @@ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.filter.js/-/svg.filter.js-3.0.9.tgz", "integrity": "sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==", "license": "MIT", - "peer": true, "dependencies": { "@svgdotjs/svg.js": "^3.2.4" }, @@ -5348,7 +5356,6 @@ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.resize.js/-/svg.resize.js-2.0.5.tgz", "integrity": "sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==", "license": "MIT", - "peer": true, "engines": { "node": ">= 14.18" }, @@ -5582,6 +5589,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.50.tgz", "integrity": "sha512-Mxiq0ULv/zo1OzOhwPqOA13I81CV/W3nvd3ChtQZRT5Cwz3cr0FKo/wMSsbTqL3EXpaBAEQhva2B8ByRkOIh9A==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -5763,6 +5771,7 @@ "integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.11.0", "@typescript-eslint/types": "7.11.0", @@ -5986,7 +5995,6 @@ "integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1" @@ -6005,7 +6013,6 @@ "integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/types": "8.32.1", "@typescript-eslint/visitor-keys": "8.32.1", @@ -6033,7 +6040,6 @@ "integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/types": "8.32.1", "eslint-visitor-keys": "^4.2.0" @@ -6052,7 +6058,6 @@ "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -6066,7 +6071,6 @@ "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=18.12" }, @@ -6293,8 +6297,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/@yr/monotone-cubic-spline/-/monotone-cubic-spline-1.0.3.tgz", "integrity": "sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/abbrev": { "version": "3.0.1", @@ -6332,6 +6335,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6390,6 +6394,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6862,6 +6867,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", @@ -8327,6 +8333,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -8383,6 +8390,7 @@ "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "license": "MIT", + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -10106,7 +10114,8 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/jest-worker": { "version": "27.5.1", @@ -10142,6 +10151,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10254,6 +10264,7 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -10385,6 +10396,7 @@ "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "jasmine-core": "^4.1.0" }, @@ -10645,6 +10657,7 @@ "resolved": "https://registry.npmjs.org/less/-/less-4.2.2.tgz", "integrity": "sha512-tkuLHQlvWUTeQ3doAqnHbNn8T6WX1KA8yvbKG9x4VtKtIjHsVKQZCH11zRgAfbDAXC2UNIg/K9BYAAcEzUIrNg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -12633,6 +12646,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -12779,6 +12793,7 @@ "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -13342,6 +13357,7 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -13395,6 +13411,7 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", "license": "MIT", + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -14533,6 +14550,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -14700,7 +14718,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/tuf-js": { "version": "3.0.1", @@ -14765,6 +14784,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -15008,7 +15028,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -15089,8 +15108,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-android-arm64": { "version": "4.41.0", @@ -15103,8 +15121,7 @@ "optional": true, "os": [ "android" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": { "version": "4.41.0", @@ -15117,8 +15134,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": { "version": "4.41.0", @@ -15131,8 +15147,7 @@ "optional": true, "os": [ "darwin" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-freebsd-arm64": { "version": "4.41.0", @@ -15145,8 +15160,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-freebsd-x64": { "version": "4.41.0", @@ -15159,8 +15173,7 @@ "optional": true, "os": [ "freebsd" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": { "version": "4.41.0", @@ -15173,8 +15186,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": { "version": "4.41.0", @@ -15187,8 +15199,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": { "version": "4.41.0", @@ -15201,8 +15212,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.41.0", @@ -15215,8 +15225,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-loongarch64-gnu": { "version": "4.41.0", @@ -15229,8 +15238,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { "version": "4.41.0", @@ -15243,8 +15251,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.41.0", @@ -15257,8 +15264,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": { "version": "4.41.0", @@ -15271,8 +15277,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.41.0", @@ -15285,8 +15290,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": { "version": "4.41.0", @@ -15299,8 +15303,7 @@ "optional": true, "os": [ "linux" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": { "version": "4.41.0", @@ -15313,8 +15316,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": { "version": "4.41.0", @@ -15327,8 +15329,7 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.41.0", @@ -15341,15 +15342,13 @@ "optional": true, "os": [ "win32" - ], - "peer": true + ] }, "node_modules/vite/node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/vite/node_modules/postcss": { "version": "8.5.3", @@ -15370,7 +15369,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -15385,7 +15383,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -15484,6 +15481,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.98.0.tgz", "integrity": "sha512-UFynvx+gM44Gv9qFgj0acCQK2VE1CtdfwFdimkapco3hlPCJ/zeq73n2yVKimVbtm+TnApIugGhLJnkU6gjYXA==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", @@ -15559,6 +15557,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.13", "@types/connect-history-api-fallback": "^1.5.4", @@ -16067,7 +16066,8 @@ "version": "0.15.0", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==", - "license": "MIT" + "license": "MIT", + "peer": true } } } diff --git a/angular-client/src/app/app-nav-bar/app-nav-bar.component.css b/angular-client/src/app/app-nav-bar/app-nav-bar.component.css index f383e912..b69c49c6 100644 --- a/angular-client/src/app/app-nav-bar/app-nav-bar.component.css +++ b/angular-client/src/app/app-nav-bar/app-nav-bar.component.css @@ -61,6 +61,33 @@ color: #ef4345; } +.navbar-notification { + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + border-radius: 4px; + position: relative; +} + +.navbar-notification mat-icon { + width: 32px; + height: 32px; + font-size: 32px; + transition: color 0.2s ease; +} + +.navbar-notification:hover { + color: #ef4345; +} + +.notification-badge { + position: absolute; + top: 2px; + right: 2px; +} + :host { --p-dialog-background: transparent !important; --p-dialog-border-color: transparent !important; diff --git a/angular-client/src/app/app-nav-bar/app-nav-bar.component.html b/angular-client/src/app/app-nav-bar/app-nav-bar.component.html index 620635f0..af1a0a73 100644 --- a/angular-client/src/app/app-nav-bar/app-nav-bar.component.html +++ b/angular-client/src/app/app-nav-bar/app-nav-bar.component.html @@ -13,6 +13,13 @@ > } + + } @else { @@ -50,6 +57,24 @@ additionalStyles="fontSize: 36px" /> + + + + + + diff --git a/angular-client/src/app/app-nav-bar/app-nav-bar.component.ts b/angular-client/src/app/app-nav-bar/app-nav-bar.component.ts index 3f6edae5..8f604df0 100644 --- a/angular-client/src/app/app-nav-bar/app-nav-bar.component.ts +++ b/angular-client/src/app/app-nav-bar/app-nav-bar.component.ts @@ -19,6 +19,10 @@ import SidebarChipComponent from 'src/components/sidebar-chip/sidebar-chip.compo import { NavOptionsMenuComponent } from 'src/components/nav-options-menu/nav-options-menu.component'; import { filter } from 'rxjs/operators'; import { RunFormComponent } from 'src/components/run-form/run-form.component'; +import { NotificationLogService } from 'src/services/notification-log.service'; +import { NotificationDropdownComponent } from 'src/components/notification-dropdown/notification-dropdown.component'; +import { Popover } from 'primeng/popover'; +import { Badge } from 'primeng/badge'; export interface NavItem { id: string; @@ -43,7 +47,10 @@ export interface NavItem { TypographyComponent, HStackComponent, SidebarChipComponent, - MatIcon + MatIcon, + NotificationDropdownComponent, + Popover, + Badge ] }) export class AppNavBarComponent implements OnInit, OnDestroy { @@ -52,6 +59,7 @@ export class AppNavBarComponent implements OnInit, OnDestroy { private router = inject(Router); private sidebarService = inject(SidebarService); private dialogService = inject(DialogService); + protected notificationLogService = inject(NotificationLogService); private subscribtions: Subscription[] = []; ref: DynamicDialogRef | undefined; @@ -189,6 +197,12 @@ export class AppNavBarComponent implements OnInit, OnDestroy { label: 'Commands', onClick: () => this.navigateTo(appRoutes.commandsRoute()), icon: 'electrical_services' + }, + { + id: appRoutes.notificationLogRoute(), + label: 'Notifications', + onClick: () => this.navigateTo(appRoutes.notificationLogRoute()), + icon: 'notifications' } ]; @@ -215,6 +229,10 @@ export class AppNavBarComponent implements OnInit, OnDestroy { this.router.navigate([route]); } + onNotificationPanelShow(): void { + this.notificationLogService.markAllRead(); + } + isSelected(navItem: NavItem) { return navItem.id === this.selectedRoute; } diff --git a/angular-client/src/app/app-routing.module.ts b/angular-client/src/app/app-routing.module.ts index 6ef5e9f0..6e9c564a 100644 --- a/angular-client/src/app/app-routing.module.ts +++ b/angular-client/src/app/app-routing.module.ts @@ -6,6 +6,7 @@ import { CameraPageComponent } from 'src/pages/camera-page/camera-page.component import CarCommandComponent from 'src/pages/car-command-page/car-command.component'; import ChargingPageComponent from 'src/pages/charging-page/charging-page.component'; import EfusesPageComponent from 'src/pages/efuses-page/efuses-page.component'; +import NotificationLogPageComponent from 'src/pages/notification-log-page/notification-log-page.component'; import FaultPageComponent from 'src/pages/fault-page/fault-page.component'; import GraphPageComponent from 'src/pages/graph-page/graph-page.component'; import LandingPageComponent from 'src/pages/landing-page/landing-page.component'; @@ -23,6 +24,7 @@ const faultsRoute = () => `/faults`; const faultsGraphRoute = () => `/faults/fault-graph`; const commandsRoute = () => `/commands`; const efusesRoute = () => `/efuses`; +const notificationLogRoute = () => `/notification-log`; export const appRoutes = { landingRoute, @@ -35,7 +37,8 @@ export const appRoutes = { faultsRoute, faultsGraphRoute, commandsRoute, - efusesRoute + efusesRoute, + notificationLogRoute }; // Routes should be defined carefully in accordance with the appRoutes @@ -52,7 +55,8 @@ const routes: Routes = [ { path: 'faults/fault-graph', component: GraphPageComponent }, { path: 'camera', component: CameraPageComponent }, { path: 'commands', component: CarCommandComponent }, - { path: 'efuses', component: EfusesPageComponent } + { path: 'efuses', component: EfusesPageComponent }, + { path: 'notification-log', component: NotificationLogPageComponent } ]; @NgModule({ diff --git a/angular-client/src/app/context/app-context.component.ts b/angular-client/src/app/context/app-context.component.ts index e833e04c..a6abb3fa 100644 --- a/angular-client/src/app/context/app-context.component.ts +++ b/angular-client/src/app/context/app-context.component.ts @@ -10,6 +10,7 @@ import { RouterOutlet } from '@angular/router'; import { MatIconRegistry } from '@angular/material/icon'; import { DomSanitizer } from '@angular/platform-browser'; import { EnvService } from 'src/services/env.service'; +import { NotificationLogService } from 'src/services/notification-log.service'; /** * Container for the entire application, contains the socket service, API serivce, and storage service. @@ -24,6 +25,7 @@ export default class AppContextComponent implements OnInit { private storage = inject(Storage); private cellService = new CellService(this.storage); private faultService = inject(FaultService); + private notificationLogService = inject(NotificationLogService); private envService = inject(EnvService); socket = io(this.envService.backendUrl, { auth: { token: 'some random token' } }); socketService = new SocketService(this.socket); @@ -94,6 +96,6 @@ export default class AppContextComponent implements OnInit { ngOnInit(): void { document.documentElement.classList.add('dark-mode-always'); this.cellService.updateCellInfo(); - this.socketService.receiveData(this.storage, this.faultService); + this.socketService.receiveData(this.storage, this.faultService, this.notificationLogService); } } diff --git a/angular-client/src/components/notification-dropdown/notification-dropdown.component.css b/angular-client/src/components/notification-dropdown/notification-dropdown.component.css new file mode 100644 index 00000000..738dc751 --- /dev/null +++ b/angular-client/src/components/notification-dropdown/notification-dropdown.component.css @@ -0,0 +1,83 @@ +.dropdown-container { + width: 360px; + max-height: 480px; + display: flex; + flex-direction: column; + background-color: #2c2c2c; + border: 1px solid #444; + border-radius: 8px; + overflow: hidden; +} + +.dropdown-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + border-bottom: 1px solid #444; +} + +.dropdown-body { + overflow-y: auto; + max-height: 360px; + flex: 1; +} + +.empty-state { + display: flex; + justify-content: center; + align-items: center; + padding: 2rem; + opacity: 0.6; +} + +.notification-item { + display: flex; + align-items: center; + padding: 10px 16px; + border-bottom: 1px solid #3a3a3a; + gap: 8px; +} + +.notification-item:last-child { + border-bottom: none; +} + +.notification-item.unread { + background-color: rgba(239, 67, 69, 0.08); +} + +.notification-content { + flex: 1; + min-width: 0; +} + +.notification-rule { + font-weight: 600; + font-size: 0.875rem; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.notification-detail { + font-size: 0.8rem; + color: #aaa; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.notification-time { + font-size: 0.75rem; + color: #777; + margin-top: 2px; +} + +.dropdown-footer { + display: flex; + justify-content: center; + padding: 8px; + border-top: 1px solid #444; +} diff --git a/angular-client/src/components/notification-dropdown/notification-dropdown.component.html b/angular-client/src/components/notification-dropdown/notification-dropdown.component.html new file mode 100644 index 00000000..7897ce0d --- /dev/null +++ b/angular-client/src/components/notification-dropdown/notification-dropdown.component.html @@ -0,0 +1,50 @@ + diff --git a/angular-client/src/components/notification-dropdown/notification-dropdown.component.ts b/angular-client/src/components/notification-dropdown/notification-dropdown.component.ts new file mode 100644 index 00000000..2291edeb --- /dev/null +++ b/angular-client/src/components/notification-dropdown/notification-dropdown.component.ts @@ -0,0 +1,24 @@ +import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; +import { Router } from '@angular/router'; +import { DatePipe } from '@angular/common'; +import { Button } from 'primeng/button'; +import { NotificationLogService } from 'src/services/notification-log.service'; +import { appRoutes } from 'src/app/app-routing.module'; +import TypographyComponent from 'src/components/typography/typography.component'; + +@Component({ + selector: 'notification-dropdown', + templateUrl: './notification-dropdown.component.html', + styleUrls: ['./notification-dropdown.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [DatePipe, Button, TypographyComponent] +}) +export class NotificationDropdownComponent { + protected notificationLogService = inject(NotificationLogService); + private router = inject(Router); + + viewAll(): void { + this.notificationLogService.markAllRead(); + this.router.navigate([appRoutes.notificationLogRoute()]); + } +} diff --git a/angular-client/src/components/notification-log-panel/notification-log-panel.component.css b/angular-client/src/components/notification-log-panel/notification-log-panel.component.css new file mode 100644 index 00000000..8bcbdc2a --- /dev/null +++ b/angular-client/src/components/notification-log-panel/notification-log-panel.component.css @@ -0,0 +1,74 @@ +.panel-container { + border: 1px solid #444; + border-radius: 8px; + background-color: #2c2c2c; + overflow: hidden; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 16px; + border-bottom: 1px solid #444; +} + +.empty-state { + display: flex; + justify-content: center; + align-items: center; + padding: 1.5rem; + opacity: 0.6; +} + +.panel-body { + max-height: 320px; + overflow-y: auto; +} + +.notification-row { + display: flex; + align-items: center; + padding: 8px 16px; + border-bottom: 1px solid #3a3a3a; + gap: 8px; +} + +.notification-row:last-child { + border-bottom: none; +} + +.notification-info { + flex: 1; + display: flex; + gap: 12px; + align-items: center; + min-width: 0; + font-size: 0.85rem; +} + +.rule-id { + font-weight: 600; + color: #fff; + white-space: nowrap; +} + +.topic { + color: #aaa; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.values { + color: #ccc; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.time { + color: #777; + white-space: nowrap; + font-size: 0.8rem; +} diff --git a/angular-client/src/components/notification-log-panel/notification-log-panel.component.html b/angular-client/src/components/notification-log-panel/notification-log-panel.component.html new file mode 100644 index 00000000..70584c56 --- /dev/null +++ b/angular-client/src/components/notification-log-panel/notification-log-panel.component.html @@ -0,0 +1,43 @@ +
+
+ + @if (notificationLogService.recentNotifications().length > 0) { + + } +
+ + @if (notificationLogService.recentNotifications().length === 0) { +
+ +
+ } @else { +
+ @for (entry of notificationLogService.recentNotifications(); track entry.id) { +
+
+ {{ entry.notification.id }} + {{ entry.notification.topic }} + {{ entry.notification.values.join(', ') }} + {{ entry.notification.time | date: 'HH:mm:ss' }} +
+ +
+ } +
+ } +
diff --git a/angular-client/src/components/notification-log-panel/notification-log-panel.component.ts b/angular-client/src/components/notification-log-panel/notification-log-panel.component.ts new file mode 100644 index 00000000..ed4c2d8c --- /dev/null +++ b/angular-client/src/components/notification-log-panel/notification-log-panel.component.ts @@ -0,0 +1,16 @@ +import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { Button } from 'primeng/button'; +import { NotificationLogService } from 'src/services/notification-log.service'; +import TypographyComponent from 'src/components/typography/typography.component'; + +@Component({ + selector: 'notification-log-panel', + templateUrl: './notification-log-panel.component.html', + styleUrls: ['./notification-log-panel.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [DatePipe, Button, TypographyComponent] +}) +export class NotificationLogPanelComponent { + protected notificationLogService = inject(NotificationLogService); +} diff --git a/angular-client/src/pages/notification-log-page/notification-log-page.component.css b/angular-client/src/pages/notification-log-page/notification-log-page.component.css new file mode 100644 index 00000000..c4ffd8bb --- /dev/null +++ b/angular-client/src/pages/notification-log-page/notification-log-page.component.css @@ -0,0 +1,31 @@ +.container { + display: flex; + flex-flow: column; + height: 100%; + padding: 8px; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.title-style { + display: grid; + align-items: center; + margin-top: -20px; +} + +.empty-state { + display: flex; + justify-content: center; + align-items: center; + padding: 3rem; + opacity: 0.6; +} + +:host ::ng-deep tr.unread td { + font-weight: 600; +} diff --git a/angular-client/src/pages/notification-log-page/notification-log-page.component.html b/angular-client/src/pages/notification-log-page/notification-log-page.component.html new file mode 100644 index 00000000..02ec231b --- /dev/null +++ b/angular-client/src/pages/notification-log-page/notification-log-page.component.html @@ -0,0 +1,55 @@ +
+
+ + @if (notificationLogService.notifications().length > 0) { + + } +
+ + @if (notificationLogService.notifications().length === 0) { +
+ +
+ } @else { + + + + Rule ID + Topic + Values + Time + Actions + + + + + {{ entry.notification.id }} + {{ entry.notification.topic }} + {{ entry.notification.values.join(', ') }} + {{ entry.notification.time | date: 'HH:mm:ss' }} + + + + + + + } +
diff --git a/angular-client/src/pages/notification-log-page/notification-log-page.component.ts b/angular-client/src/pages/notification-log-page/notification-log-page.component.ts new file mode 100644 index 00000000..1540c639 --- /dev/null +++ b/angular-client/src/pages/notification-log-page/notification-log-page.component.ts @@ -0,0 +1,17 @@ +import { Component, ChangeDetectionStrategy, inject } from '@angular/core'; +import { DatePipe } from '@angular/common'; +import { TableModule } from 'primeng/table'; +import { Button } from 'primeng/button'; +import TypographyComponent from 'src/components/typography/typography.component'; +import { NotificationLogService } from 'src/services/notification-log.service'; + +@Component({ + selector: 'notification-log-page', + templateUrl: './notification-log-page.component.html', + styleUrls: ['./notification-log-page.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [TableModule, DatePipe, Button, TypographyComponent] +}) +export default class NotificationLogPageComponent { + protected notificationLogService = inject(NotificationLogService); +} diff --git a/angular-client/src/services/notification-log.service.ts b/angular-client/src/services/notification-log.service.ts new file mode 100644 index 00000000..197f0faf --- /dev/null +++ b/angular-client/src/services/notification-log.service.ts @@ -0,0 +1,43 @@ +import { Injectable, signal, computed } from '@angular/core'; +import { RuleNotification } from 'src/utils/types.utils'; + +export interface NotificationLogEntry { + id: number; + notification: RuleNotification; + read: boolean; +} + +@Injectable({ + providedIn: 'root' +}) +export class NotificationLogService { + private static readonly MAX_ENTRIES = 500; + + private entries = signal([]); + private nextId = 0; + + readonly notifications = computed(() => this.entries()); + readonly unreadCount = computed(() => this.entries().filter((e) => !e.read).length); + readonly recentNotifications = computed(() => this.entries().slice(0, 10)); + + addNotification(notification: RuleNotification): void { + this.entries.update((current) => { + const updated = [{ id: this.nextId++, notification, read: false }, ...current]; + return updated.length > NotificationLogService.MAX_ENTRIES + ? updated.slice(0, NotificationLogService.MAX_ENTRIES) + : updated; + }); + } + + markAllRead(): void { + this.entries.update((current) => current.map((e) => ({ ...e, read: true }))); + } + + dismissEntry(entryId: number): void { + this.entries.update((current) => current.filter((e) => e.id !== entryId)); + } + + clearAll(): void { + this.entries.set([]); + } +} diff --git a/angular-client/src/services/socket.service.ts b/angular-client/src/services/socket.service.ts index 9f6dd985..327e8040 100644 --- a/angular-client/src/services/socket.service.ts +++ b/angular-client/src/services/socket.service.ts @@ -2,8 +2,9 @@ import { Socket } from 'socket.io-client'; import { DataValue, ServerData, TimerData } from 'src/utils/socket.utils'; import Storage from './storage.service'; import { topics } from 'src/utils/topic.utils'; -import { FaultData } from 'src/utils/types.utils'; +import { FaultData, RuleNotification } from 'src/utils/types.utils'; import { FaultService } from './fault.service'; +import { NotificationLogService } from './notification-log.service'; /** * Service for interacting with the socket @@ -23,7 +24,7 @@ export default class SocketService { /** * Subscribe to the 'message' event from the server */ - receiveData = (storage: Storage, faultService: FaultService) => { + receiveData = (storage: Storage, faultService: FaultService, notificationLogService: NotificationLogService) => { this.socket.on('data', (message: string) => { try { const data = JSON.parse(message) as ServerData; @@ -78,6 +79,15 @@ export default class SocketService { } }); + this.socket.on('rule_notify', (message: string) => { + try { + const data = JSON.parse(message) as RuleNotification; + notificationLogService.addNotification(data); + } catch (error) { + if (error instanceof Error) this.sendError(error.message); + } + }); + this.socket.on('disconnect', () => { storage.setCurrentRunId(undefined); }); diff --git a/angular-client/src/utils/types.utils.ts b/angular-client/src/utils/types.utils.ts index 705c6197..2e542e22 100644 --- a/angular-client/src/utils/types.utils.ts +++ b/angular-client/src/utils/types.utils.ts @@ -91,3 +91,10 @@ export interface Timing { before: number; after: number; } + +export interface RuleNotification { + id: string; + topic: string; + values: number[]; + time: string; +} From 099ee084ad1d7cec7fcf73f63d7af927a0f0d18b Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 29 Mar 2026 14:22:47 -0400 Subject: [PATCH 2/3] #544 cap entries and simplify signal accessor --- angular-client/src/services/notification-log.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/angular-client/src/services/notification-log.service.ts b/angular-client/src/services/notification-log.service.ts index 197f0faf..f37728c0 100644 --- a/angular-client/src/services/notification-log.service.ts +++ b/angular-client/src/services/notification-log.service.ts @@ -16,7 +16,7 @@ export class NotificationLogService { private entries = signal([]); private nextId = 0; - readonly notifications = computed(() => this.entries()); + readonly notifications = this.entries.asReadonly(); readonly unreadCount = computed(() => this.entries().filter((e) => !e.read).length); readonly recentNotifications = computed(() => this.entries().slice(0, 10)); From 3c9b46019aa960cea51196a0e6b799b9be28a3ee Mon Sep 17 00:00:00 2001 From: wyattb Date: Sun, 29 Mar 2026 19:20:11 -0400 Subject: [PATCH 3/3] #544 simplify notification log service and dropdown --- .../notification-dropdown.component.html | 4 ++-- .../src/services/notification-log.service.ts | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/angular-client/src/components/notification-dropdown/notification-dropdown.component.html b/angular-client/src/components/notification-dropdown/notification-dropdown.component.html index 7897ce0d..7c3e7366 100644 --- a/angular-client/src/components/notification-dropdown/notification-dropdown.component.html +++ b/angular-client/src/components/notification-dropdown/notification-dropdown.component.html @@ -1,7 +1,7 @@