samanbayaka 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,72 @@
1
+ /*format-error.mjs*/
2
+
3
+ import { Errors } from "moleculer"
4
+
5
+ export const formatBrokerErrors = (err, info)=>{
6
+
7
+ const traceId = info?.ctx?.requestID
8
+ const status = err?.code || 500
9
+ const type = err?.type || 'INTERNAL_ERROR'
10
+ const instance = info?.ctx?.action?.name
11
+ ? `${info?.ctx?.action?.name}`
12
+ : `event.${info?.ctx?.event?.name}`
13
+ const title = err?.message || ''
14
+ const detail = (err?.data && Array.isArray(err?.data))
15
+ ? err.data.map(el=>{
16
+ delete el.nodeID
17
+ delete el.action
18
+ return el
19
+ })
20
+ : err?.data || ""
21
+
22
+ const stack = err?.stack || ""
23
+ const timestamp = new Date().toISOString()
24
+
25
+ if(err.nodeID === undefined){
26
+ if(err.code){
27
+ info.service.broker.logger.error('❖ SBK', {traceId, type, instance, detail, stack: ""})
28
+ }
29
+ else{
30
+ info.service.broker.logger.error('❖ SBK', {traceId, type, instance, detail, stack})
31
+ }
32
+
33
+ /**
34
+ * If it's an event, do not throw. Just log and stop.
35
+ */
36
+ if (info?.ctx?.event) {
37
+ return
38
+ }
39
+
40
+ throw new Errors.MoleculerError(
41
+ title,
42
+ status,
43
+ type,
44
+ {
45
+ traceId,
46
+ status,
47
+ type,
48
+ title,
49
+ detail,
50
+ timestamp
51
+ }
52
+ )
53
+ }
54
+ throw err
55
+ }
56
+
57
+ export const formatApiGwErrors = (req, res, err)=>{
58
+ const code = err.code || 500
59
+
60
+ /**
61
+ * Handle only server errors (>=500)
62
+ */
63
+ res.setHeader("Content-Type", "application/json")
64
+ res.writeHead(code)
65
+ res.end(JSON.stringify({
66
+ error: {
67
+ ...err?.data || {message: err.message},
68
+ ...{instance: `${req.method} ${req.url}`}
69
+ },
70
+ })
71
+ )
72
+ }
@@ -0,0 +1,55 @@
1
+ /*node-argv.mjs*/
2
+
3
+ import os from "os"
4
+
5
+ /**
6
+ * Remove the first two elements that used by the node itself from
7
+ * the arguments and define it as a constant
8
+ * @type {array}
9
+ */
10
+ const argv = process.argv.slice(2)
11
+
12
+
13
+ /**
14
+ * Extract the service name from the arguments.
15
+ * @type {string}
16
+ */
17
+ export const serviceName = argv.find(el => /^[a-z0-9]+(-[a-z0-9]+)*$/i.test(el))
18
+
19
+
20
+ /**
21
+ * An error is logged when the argument does not contain a valid service name.
22
+ */
23
+ if (serviceName === undefined) {
24
+ console.error("Error: Service name is missing.")
25
+ console.log("Please run one of the following commands:\n $node your_file.mjs <service_name> -h (for hot reloading)\n $node your_file.mjs <service_name> --hot (for hot reloading)\n or \n $node your_file.mjs <service_name>")
26
+ process.exit(1)
27
+ }
28
+
29
+
30
+ /**
31
+ * Extract the hot-reloading switch from the arguments and define
32
+ * it as a constant
33
+ * @type {Boolean}
34
+ */
35
+ export const isHotReloadEnabled = (argv.includes("-h") || argv.includes("--hot")) || false
36
+
37
+
38
+ /**
39
+ * Extract the REPL switch from the arguments and define
40
+ * it as a constant
41
+ * @type {Boolean}
42
+ */
43
+ export const isReplEnabled = (argv.includes("-r") || argv.includes("--repl")) || false
44
+
45
+
46
+ /**
47
+ * Generate a unique ID for each node instance.
48
+ * Combine the service name, host name, and process ID
49
+ * @type {string}
50
+ */
51
+ export const nodeUid = [
52
+ isHotReloadEnabled ? `${serviceName}^` : serviceName,
53
+ os.hostname(),
54
+ process.pid
55
+ ].join(".")
@@ -0,0 +1,65 @@
1
+ /*AjvValidator.mjs*/
2
+
3
+ import { randomUUID } from "crypto"
4
+
5
+ import Ajv from "ajv"
6
+ import { Validators, Errors } from "moleculer"
7
+
8
+ export default class AjvValidator extends Validators.Base {
9
+ constructor() {
10
+ super()
11
+ this.ajv = new Ajv({
12
+ allErrors: true,
13
+ strict: false,
14
+ coerceTypes: true //true auto fit data to integer data type like 12 if supplied "12"
15
+ })
16
+ }
17
+
18
+ compile(schema) {
19
+ const validate = this.ajv.compile(schema)
20
+
21
+ return (params) => {
22
+ const valid = validate(params)
23
+
24
+ if (!valid) {
25
+ return formatAjvErrors(validate.errors, params)
26
+ }
27
+ return true
28
+ }
29
+ }
30
+ }
31
+
32
+
33
+ /**
34
+ * Format AJV Errors
35
+ * @param {error object}
36
+ * @param {params object}
37
+ * @return {object}
38
+ */
39
+ const formatAjvErrors = (errors, params) => {
40
+ if(!errors){
41
+ return false
42
+ }
43
+
44
+ return errors.map(err => {
45
+ /**
46
+ * Remove leading "/" and subsequent "/" to "."
47
+ * @type {string}
48
+ */
49
+ const ky = err.instancePath
50
+ .replace(/^\/+/, '')
51
+ .replace(/\//g, '.')
52
+
53
+ /**
54
+ * Getting value from nested object
55
+ * @param {string} like query.name.lastName
56
+ * @return {string}
57
+ */
58
+ const val = ky.split('.').reduce((acc, key) => acc?.[key], params)
59
+
60
+ return {
61
+ field: ky,
62
+ message: `The value '${val}' ${err.message}`
63
+ }
64
+ })
65
+ }
package/index-argv.mjs ADDED
@@ -0,0 +1,92 @@
1
+ /*index.mjs*/
2
+ import path from "path"
3
+ import url from "url"
4
+ import fs from "fs"
5
+
6
+ import { ServiceBroker } from "moleculer"
7
+ import { execSync } from "child_process"
8
+
9
+ import {isHotReloadEnabled, serviceName, nodeUid} from '#hUti/node-argv.mjs'
10
+ import {formatBrokerErrors} from '#hUti/format-errors.mjs'
11
+ import {getConfig, loadServices} from '#hFil/esm-loading.mjs'
12
+ import AjvValidator from "#hVal/AjvValidator.mjs"
13
+
14
+ const BROKER_CONFIG = await getConfig('broker')
15
+ const NATS_CONFIG = await getConfig('nats')
16
+ const LOGGER_CONFIG = await getConfig('logger')
17
+
18
+ /**
19
+ * Last git commit hash in short form
20
+ */
21
+ const gitCommitHs = execSync("git rev-parse --short HEAD")
22
+ .toString()
23
+ .trim()
24
+
25
+
26
+ /**
27
+ * Moleculer srvice broker configurations
28
+ * @type {ServiceBroker}
29
+ */
30
+ const broker = new ServiceBroker({
31
+ ...{
32
+ ...BROKER_CONFIG,
33
+ namespace: `${BROKER_CONFIG.namespace}-${gitCommitHs}`
34
+ },
35
+ ...{
36
+ nodeID: nodeUid,
37
+ transporter: NATS_CONFIG,
38
+ validator: new AjvValidator(),
39
+ logger: LOGGER_CONFIG,
40
+ },
41
+ errorHandler: formatBrokerErrors,
42
+ })
43
+
44
+
45
+ /**
46
+ * Loads modules asynchronously
47
+ */
48
+ await loadServices(broker)
49
+
50
+
51
+ /**
52
+ * Start broker with repl mode
53
+ */
54
+ broker.start().then(() => broker.repl())
55
+
56
+
57
+ /**
58
+ * Graceful shutdown handler
59
+ */
60
+ const shutdown = async (signal) => {
61
+ broker.logger.info('❖ SBK', `Received ${signal}. Stopping broker...`)
62
+
63
+ try {
64
+ await broker.stop()
65
+ broker.logger.info('❖ SBK', "Broker stopped gracefully.")
66
+ process.exit(0)
67
+ } catch (err) {
68
+ broker.logger.error('❖ SBK', "Error during broker shutdown:", err)
69
+ process.exit(1)
70
+ }
71
+ }
72
+
73
+
74
+ /**
75
+ * Listen for force stop signals
76
+ */
77
+ process.on("SIGINT", shutdown) // Ctrl+C
78
+ process.on("SIGTERM", shutdown) // kill command
79
+
80
+
81
+ /**
82
+ * Optional: handle uncaught errors
83
+ */
84
+ process.on("uncaughtException", async (err) => {
85
+ broker.logger.error('❖ SBK', "Uncaught Exception:", err)
86
+ await shutdown("uncaughtException")
87
+ })
88
+
89
+ process.on("unhandledRejection", async (err) => {
90
+ broker.logger.error('❖ SBK', "Unhandled Rejection:", err)
91
+ await shutdown("unhandledRejection")
92
+ })
package/index.mjs ADDED
@@ -0,0 +1,121 @@
1
+ /*index.mjs*/
2
+ import path from "path"
3
+ import url from "url"
4
+ import fs from "fs"
5
+
6
+ import { ServiceBroker } from "moleculer"
7
+ import { execSync } from "child_process"
8
+
9
+ import {
10
+ isReplEnabled,
11
+ isHotReloadEnabled,
12
+ serviceName,
13
+ nodeUid
14
+ } from '#hUti/node-argv.mjs'
15
+ import {formatBrokerErrors} from '#hUti/format-errors.mjs'
16
+ import {
17
+ getConfig,
18
+ loadServices
19
+ } from '#hFil/esm-loading.mjs'
20
+ import AjvValidator from "#hVal/AjvValidator.mjs"
21
+
22
+ const BROKER_CONFIG = await getConfig('broker')
23
+ const NATS_CONFIG = await getConfig('nats')
24
+ const LOGGER_CONFIG = await getConfig('logger')
25
+
26
+ /**
27
+ * Last git commit hash in short form
28
+ */
29
+ const gitCommitHs = execSync("git rev-parse --short HEAD")
30
+ .toString()
31
+ .trim()
32
+
33
+
34
+ /**
35
+ * Moleculer srvice broker configurations
36
+ * @type {ServiceBroker}
37
+ */
38
+ const broker = new ServiceBroker({
39
+ ...{
40
+ ...BROKER_CONFIG,
41
+ ...{
42
+ namespace: `${BROKER_CONFIG.namespace}-${gitCommitHs}`
43
+ }
44
+ },
45
+ ...{
46
+ nodeID: nodeUid,
47
+ transporter: NATS_CONFIG,
48
+ validator: new AjvValidator(),
49
+ logger: LOGGER_CONFIG,
50
+ },
51
+ errorHandler: formatBrokerErrors,
52
+ })
53
+
54
+
55
+ // /**
56
+ // * Loads modules asynchronously
57
+ // */
58
+ // await loadServices(broker)
59
+
60
+
61
+ // /**
62
+ // * Start broker with repl mode
63
+ // */
64
+ // if(isReplEnabled){
65
+ // broker.start().then(() => broker.repl())
66
+ // }
67
+ // else{
68
+ // broker.start()
69
+ // }
70
+
71
+ export const sbk = async (sBus, schema)=>{
72
+ broker.createService(schema)
73
+
74
+ /**
75
+ * Start broker with repl mode
76
+ */
77
+ if(isReplEnabled){
78
+ broker.start().then(() => broker.repl())
79
+ }
80
+ else{
81
+ broker.start()
82
+ }
83
+ }
84
+
85
+
86
+ /**
87
+ * Graceful shutdown handler
88
+ */
89
+ const shutdown = async (signal) => {
90
+ broker.logger.info('❖ SBK', `Received ${signal}. Stopping broker...`)
91
+
92
+ try {
93
+ await broker.stop()
94
+ broker.logger.info('❖ SBK', "Broker stopped gracefully.")
95
+ process.exit(0)
96
+ } catch (err) {
97
+ broker.logger.error('❖ SBK', "Error during broker shutdown:", err)
98
+ process.exit(1)
99
+ }
100
+ }
101
+
102
+
103
+ /**
104
+ * Listen for force stop signals
105
+ */
106
+ process.on("SIGINT", shutdown) // Ctrl+C
107
+ process.on("SIGTERM", shutdown) // kill command
108
+
109
+
110
+ /**
111
+ * Optional: handle uncaught errors
112
+ */
113
+ process.on("uncaughtException", async (err) => {
114
+ broker.logger.error('❖ SBK', "Uncaught Exception:", err)
115
+ await shutdown("uncaughtException")
116
+ })
117
+
118
+ process.on("unhandledRejection", async (err) => {
119
+ broker.logger.error('❖ SBK', "Unhandled Rejection:", err)
120
+ await shutdown("unhandledRejection")
121
+ })
package/metrics.mjs ADDED
@@ -0,0 +1,49 @@
1
+ /*metrics.mjs*/
2
+ import { ServiceBroker } from "moleculer"
3
+ import { logLevel } from "kafkajs"
4
+ import os from "os"
5
+
6
+ import CONFIG from "./config/service.config.mjs"
7
+ import RedpandaTransporter from "./transporter/RedpandaTransporter.mjs"
8
+ import KAFKAJS_CONFIG from "./config/kafkajs.config.mjs"
9
+
10
+ const serviceName = "metrics"
11
+
12
+ const getNodeId = () => {
13
+ const hostName = os.hostname()
14
+ // return `${serviceName}.${hostName}.${process.pid}`
15
+ return `${serviceName}.${hostName}`
16
+ }
17
+
18
+ const broker = new ServiceBroker({
19
+ nodeID: getNodeId(),
20
+ transporter: new RedpandaTransporter(KAFKAJS_CONFIG),
21
+ metrics: true, // enable metrics
22
+ tracing: {
23
+ enabled: true,
24
+ reporter: [
25
+ {
26
+ type: "Prometheus",
27
+ options: {
28
+ port: 8088,
29
+ path: "/metrics",
30
+ defaultLabels: (registry) => ({
31
+ nodeID: registry.broker.nodeID
32
+ })
33
+ }
34
+ }
35
+ ]
36
+ },
37
+ // Enable tracing with console exporter (valid types: Console, Jaeger, Datadog, Zipkin)
38
+ tracing: {
39
+ enabled: true,
40
+ exporter: [
41
+ {
42
+ type: "Console"
43
+ }
44
+ ]
45
+ }
46
+ })
47
+
48
+ /*Start broker*/
49
+ broker.start()
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "samanbayaka",
3
+ "version": "0.0.2",
4
+ "description": "Moleculer Gateway service with kafka transporter",
5
+ "homepage": "https://gitlab.com/dalal.suvendu/samanbayaka#readme",
6
+ "bugs": {
7
+ "url": "https://gitlab.com/dalal.suvendu/samanbayaka/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://gitlab.com/dalal.suvendu/samanbayaka.git"
12
+ },
13
+ "license": "ISC",
14
+ "author": "dalal.suvendu",
15
+ "type": "module",
16
+ "imports": {
17
+ "#hFil/*": "./helper/file/*",
18
+ "#hUti/*": "./helper/utility/*",
19
+ "#hVal/*": "./helper/validator/*",
20
+ "#sSys/*": "./services/system/*"
21
+ },
22
+ "main": "index.mjs",
23
+ "scripts": {
24
+ "test": "echo \"Error: no test specified\" && exit 1",
25
+ "start": "node index.mjs service_name"
26
+ },
27
+ "dependencies": {
28
+ "ajv": "^8.18.0",
29
+ "chokidar": "^5.0.0",
30
+ "compression": "^1.8.1",
31
+ "cookie-parser": "^1.4.7",
32
+ "helmet": "^8.1.0",
33
+ "kafkajs": "^2.2.4",
34
+ "moleculer": "^0.14.35",
35
+ "moleculer-auto-openapi": "^1.1.7",
36
+ "moleculer-repl": "^0.7.4",
37
+ "moleculer-web": "^0.10.8",
38
+ "msgpack5": "^6.0.2",
39
+ "nats": "^2.29.3",
40
+ "pnpm": "^10.32.1",
41
+ "swagger-stats": "^0.99.7"
42
+ }
43
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "samanbayaka-gateway",
3
+ "version": "0.0.1",
4
+ "description": "Moleculer Gateway service with kafka transporter",
5
+ "homepage": "https://gitlab.com/dalal.suvendu/samanbayaka#readme",
6
+ "bugs": {
7
+ "url": "https://gitlab.com/dalal.suvendu/samanbayaka/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://gitlab.com/dalal.suvendu/samanbayaka.git"
12
+ },
13
+ "license": "ISC",
14
+ "author": "dalal.suvendu",
15
+ "type": "module",
16
+ "main": "index.mjs",
17
+ "scripts": {
18
+ "test": "echo \"Error: no test specified\" && exit 1",
19
+ "start": "node index.mjs service_name"
20
+ },
21
+ "dependencies": {
22
+ "chokidar": "^4.0.3",
23
+ "cookie-parser": "^1.4.7",
24
+ "helmet": "^8.1.0",
25
+ "kafkajs": "^2.2.4",
26
+ "moleculer": "^0.14.35",
27
+ "moleculer-auto-openapi": "^1.1.6",
28
+ "moleculer-repl": "^0.7.4",
29
+ "moleculer-web": "^0.10.8",
30
+ "swagger-stats": "^0.99.7"
31
+ }
32
+ }
@@ -0,0 +1,2 @@
1
+ onlyBuiltDependencies:
2
+ - es5-ext
Binary file
@@ -0,0 +1,30 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 80 80" fill="none" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink">
2
+ <rect x="2" y="2" rx="25" width="76" height="76" fill="none" stroke="#b" stroke-width="2.2"/>
3
+ <g transform="rotate(45, 40, 40) scale(.8) translate(10, 10)">
4
+ <!--<rect x="2" y="2" width="12" height="12" rx="2" fill="#f2fabd" stroke-width="1" stroke="#f2fabd"/>-->
5
+ <rect x="18" y="2" width="12" height="12" rx="2" fill="#ecf9a5" stroke-width="1" stroke="#ecf9a5"/>
6
+ <rect x="34" y="2" width="12" height="12" rx="2" fill="#e7f792" stroke-width="1" stroke="#e7f792"/>
7
+ <rect x="50" y="2" width="12" height="12" rx="2" fill="#e3f786" stroke-width="1" stroke="#e3f786"/>
8
+ <!--<rect x="66" y="2" width="12" height="12" rx="2" fill="#cfe07a" stroke-width="1" stroke="#cfe07a"/>-->
9
+ <rect x="2" y="18" width="12" height="12" rx="2" fill="#ffedb6" stroke-width="1" stroke="#ffedb6"/>
10
+ <rect x="18" y="18" width="12" height="12" rx="2" fill="#ffe79b" stroke-width="1" stroke="#ffe79b"/>
11
+ <rect x="34" y="18" width="12" height="12" rx="2" fill="#ffe286" stroke-width="1" stroke="#ffe286"/>
12
+ <rect x="50" y="18" width="12" height="12" rx="2" fill="#ffdf78" stroke-width="1" stroke="#ffdf78"/>
13
+ <rect x="66" y="18" width="12" height="12" rx="2" fill="#ebcb6e" stroke-width="1" stroke="#ebcb6e"/>
14
+ <rect x="2" y="34" width="12" height="12" rx="2" fill="#ffd09d" stroke-width="1" stroke="#ffd09d"/>
15
+ <rect x="18" y="34" width="12" height="12" rx="2" fill="#ffbf79" stroke-width="1" stroke="#ffbf79"/>
16
+ <rect x="34" y="34" width="12" height="12" rx="2" fill="#ffb25e" stroke-width="1" stroke="#ffb25e"/>
17
+ <rect x="50" y="34" width="12" height="12" rx="2" fill="#ffaa4c" stroke-width="1" stroke="#ffaa4c"/>
18
+ <rect x="66" y="34" width="12" height="12" rx="2" fill="#ef9b46" stroke-width="1" stroke="#ef9b46"/>
19
+ <rect x="2" y="50" width="12" height="12" rx="2" fill="#ffac8b" stroke-width="1" stroke="#ffac8b"/>
20
+ <rect x="18" y="50" width="12" height="12" rx="2" fill="#ff8c63" stroke-width="1" stroke="#ff8c63"/>
21
+ <rect x="34" y="50" width="12" height="12" rx="2" fill="#ff7245" stroke-width="1" stroke="#ff7245"/>
22
+ <rect x="50" y="50" width="12" height="12" rx="2" fill="#ff6030" stroke-width="1" stroke="#ff6030"/>
23
+ <rect x="66" y="50" width="12" height="12" rx="2" fill="#ec582d" stroke-width="1" stroke="#ec582d"/>
24
+ <!--<rect x="2" y="66" width="12" height="12" rx="2" fill="#fb9495" stroke-width="1" stroke="#fb9495"/>-->
25
+ <rect x="18" y="66" width="12" height="12" rx="2" fill="#f36a71" stroke-width="1" stroke="#f36a71"/>
26
+ <rect x="34" y="66" width="12" height="12" rx="2" fill="#eb4558" stroke-width="1" stroke="#eb4558"/>
27
+ <rect x="50" y="66" width="12" height="12" rx="2" fill="#e41f47" stroke-width="1" stroke="#e41f47"/>
28
+ <!--<rect x="66" y="66" width="12" height="12" rx="2" fill="#d02142" stroke-width="1" stroke="#d02142"/>-->
29
+ </g>
30
+ </svg>
Binary file
@@ -0,0 +1,115 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Dashboard with Vertical Tabs</title>
6
+ <style>
7
+ :root {
8
+ --sidebar-width: 220px;
9
+ --bg: #0f1720;
10
+ --sidebar-bg: #111827;
11
+ --tab-hover: #1f2937;
12
+ --active: #2563eb;
13
+ --text: #e5e7eb;
14
+ --muted: #9ca3af;
15
+ font-family: sans-serif;
16
+ }
17
+
18
+ body {
19
+ margin: 0;
20
+ height: 100vh;
21
+ display: grid;
22
+ grid-template-columns: var(--sidebar-width) 1fr;
23
+ background: var(--bg);
24
+ color: var(--text);
25
+ }
26
+
27
+ /* Sidebar */
28
+ .sidebar {
29
+ display: flex;
30
+ flex-direction: column;
31
+ background: var(--sidebar-bg);
32
+ padding: 12px;
33
+ gap: 8px;
34
+ }
35
+
36
+ /* Sidebar brand */
37
+ .brand {
38
+ font-weight: bold;
39
+ font-size: 18px;
40
+ margin-bottom: 16px;
41
+ color: var(--active);
42
+ }
43
+
44
+ /* Tabs */
45
+ .tab {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 10px;
49
+ padding: 10px;
50
+ border-radius: 8px;
51
+ cursor: pointer;
52
+ color: var(--muted);
53
+ transition: background 0.2s, color 0.2s;
54
+ }
55
+ .tab:hover { background: var(--tab-hover); color: var(--text); }
56
+ .tab.active { background: var(--active); color: white; }
57
+
58
+ /* Main content */
59
+ .main {
60
+ display: flex;
61
+ flex-direction: column;
62
+ padding: 20px;
63
+ overflow: auto;
64
+ }
65
+
66
+ header {
67
+ font-size: 20px;
68
+ font-weight: bold;
69
+ margin-bottom: 20px;
70
+ }
71
+ section {
72
+ flex: 1;
73
+ background: #1f2937;
74
+ border-radius: 10px;
75
+ padding: 20px;
76
+ }
77
+ </style>
78
+ </head>
79
+ <body>
80
+ <aside class="sidebar">
81
+ <div class="brand">🔥 Dashboard</div>
82
+ <div class="tab active" onclick="showPage('overview')">📊 Overview</div>
83
+ <div class="tab" onclick="showPage('reports')">📑 Reports</div>
84
+ <div class="tab" onclick="showPage('settings')">⚙️ Settings</div>
85
+ <div class="tab" onclick="showPage('profile')">👤 Profile</div>
86
+ </aside>
87
+
88
+ <main class="main">
89
+ <header id="pageTitle">Overview</header>
90
+ <section id="pageContent">
91
+ <p>Welcome to the dashboard. Select a tab on the left to switch content.</p>
92
+ </section>
93
+ </main>
94
+
95
+ <script>
96
+ const tabs = document.querySelectorAll('.tab');
97
+ const pageTitle = document.getElementById('pageTitle');
98
+ const pageContent = document.getElementById('pageContent');
99
+
100
+ function showPage(name) {
101
+ tabs.forEach(tab => tab.classList.remove('active'));
102
+ const clicked = [...tabs].find(t => t.textContent.includes(name[0].toUpperCase() + name.slice(1)));
103
+ if (clicked) clicked.classList.add('active');
104
+
105
+ pageTitle.textContent = name.charAt(0).toUpperCase() + name.slice(1);
106
+ pageContent.innerHTML = {
107
+ overview: "<p>Dashboard overview metrics go here.</p>",
108
+ reports: "<p>Reports section with charts and data tables.</p>",
109
+ settings: "<p>User and system settings here.</p>",
110
+ profile: "<p>Profile information and account settings.</p>"
111
+ }[name];
112
+ }
113
+ </script>
114
+ </body>
115
+ </html>