diff --git a/build.mjs b/build.mjs index bead412d3f..63f94f1ce4 100644 --- a/build.mjs +++ b/build.mjs @@ -31,16 +31,11 @@ import sharp from 'sharp'; const __dirname = dirname(fileURLToPath(import.meta.url)); const isProd = process.argv.includes('--prod'); -const terserOptions = { - compress: { drop_console: true }, - output: { ecma: 5 }, -}; - /** Shared SCSS / PostCSS options */ const sharedCSS = { preprocessorOptions: { scss: { - silenceDeprecations: ['import', 'global-builtin', 'color-functions', 'mixed-decls'], + silenceDeprecations: ['import', 'global-builtin', 'color-functions', 'if-function'], } }, postcss: { @@ -91,7 +86,7 @@ const imageminPlugin = isProd ? { /** Asset output path rules */ const assetFileNames = (assetInfo) => { - const name = assetInfo.name || ''; + const name = assetInfo.names?.[0] || ''; if (/\.(png|gif|svg|jpg|jpeg|ico)$/i.test(name)) return 'images/[name][extname]'; if (/\.(woff2?|ttf|eot|otf)$/i.test(name)) return 'assets/[name][extname]'; return '[name][extname]'; @@ -107,13 +102,29 @@ const handleEvalFiles = { } }; +/* Suppress eval warnings from third-party files as we cannot fix them */ +const onwarnSuppressEval = (warning, defaultHandler) => { + if (warning.code === 'EVAL' && warning.id && + (warning.id.includes('store-js/plugins/lib/json2.js') || + warning.id.includes('jquery.tablesorter.js'))) { + return; + } + defaultHandler(warning); +}; + /** * The inject plugin adds `import $ from 'jquery'` (and jQuery / moment) * to any module that uses those names as free variables without importing them. * This covers legacy vendor scripts (bootstrap-datatable, bootstrap-select, * jquery.tablesorter, etc.) that assume jQuery is available as a global. */ -const injectGlobals = inject({ $: 'jquery', jQuery: 'jquery', moment: 'moment-timezone' }); +const injectGlobals = inject({ + $: 'jquery', + jQuery: 'jquery', + moment: 'moment-timezone', + include: ['**/*.js', '**/*.ts', '**/*.vue', '**/*.mjs'], + exclude: ['**/*.css', '**/*.scss', '**/*.sass'], +}); // Build 1: third-party.js // Self-contained IIFE — bundles jQuery, Bootstrap, DataTables, Leaflet, etc. @@ -131,11 +142,11 @@ await build({ emptyOutDir: true, // wipe dist only on the first build step cssCodeSplit: false, // extract CSS to a file (not inline via __vite_style__) sourcemap: !isProd, - minify: isProd ? 'terser' : false, - terserOptions: isProd ? terserOptions : undefined, + minify: isProd ? 'esbuild' : false, chunkSizeWarningLimit: 5000, rollupOptions: { context: 'window', + onwarn: onwarnSuppressEval, input: { 'third-party': resolve(__dirname, 'assets/third-party.js') }, output: { format: 'iife', @@ -163,10 +174,10 @@ await build({ emptyOutDir: false, cssCodeSplit: false, // extract CSS to a file (not inline via __vite_style__) sourcemap: !isProd, - minify: isProd ? 'terser' : false, - terserOptions: isProd ? terserOptions : undefined, + minify: isProd ? 'esbuild' : false, chunkSizeWarningLimit: 5000, rollupOptions: { + onwarn: onwarnSuppressEval, input: { ntopng: resolve(__dirname, 'http_src/ntopng.js') }, external: ['jquery', 'moment', 'moment-timezone'], output: { @@ -205,7 +216,7 @@ for (const { entry, name } of cssEntries) { outDir: 'httpdocs/dist', emptyOutDir: false, cssCodeSplit: false, // extract CSS to a file (not inline via __vite_style__) - minify: isProd ? 'terser' : false, + minify: isProd ? 'esbuild' : false, rollupOptions: { input: { [name]: resolve(__dirname, entry) }, output: { @@ -249,8 +260,7 @@ await build({ build: { outDir: 'httpdocs/dist', emptyOutDir: false, - minify: isProd ? 'terser' : false, - terserOptions: isProd ? terserOptions : undefined, + minify: isProd ? 'esbuild' : false, rollupOptions: { input: { login: resolve(__dirname, 'assets/scripts/login.js') }, output: { diff --git a/create_dist.sh b/create_dist.sh index e334ff4f63..550a27f946 100755 --- a/create_dist.sh +++ b/create_dist.sh @@ -1,35 +1,26 @@ #!/bin/bash -# -# In case you have never used npm do -# -# npm install -# - CURR_DIR=$(pwd) -branch_name=`git branch | head | cut -d ' ' -f 2 | tail -n 1` - -echo "-- Cleaning up dist -- " +branch_name=$(git branch --show-current) +echo "Dist Branch: $branch_name" +echo "-- Cleaning up dist --" cd httpdocs/dist git fetch -git checkout $branch_name -git reset --hard @{u} +git checkout -B "$branch_name" "origin/$branch_name" -echo "-- Compiling dist -- " -cd $CURR_DIR +echo "-- Compiling dist --" +cd "$CURR_DIR" npm run build || exit 1 echo "-- Pushing dist --" cd httpdocs/dist -git add * +git add -A git commit -m 'Update dist' || exit 1 -git push || exit 1 +git push origin "$branch_name" || exit 1 echo "-- Pushing ref --" -cd $CURR_DIR +cd "$CURR_DIR" git add httpdocs/dist git commit -m 'Update dist' || exit 1 git push || exit 1 - -echo "Dist up to date" diff --git a/http_src/utilities/ntop-utils.js b/http_src/utilities/ntop-utils.js index 3a86bcdbbd..af38bf08ad 100644 --- a/http_src/utilities/ntop-utils.js +++ b/http_src/utilities/ntop-utils.js @@ -1329,38 +1329,43 @@ export default class NtopUtils { } static createProgressBar(percentage) { - return `
-
-
-
-
-
 ${percentage} %
-
` + const pct = Math.min(100, Math.max(0, Math.floor(percentage))); + const color = pct >= 80 ? 'var(--ntop-orange, #FF8F00)' : pct >= 50 ? '#f59e0b' : 'var(--ntop-blue, #37474F)'; + return `
+
+
+
+ ${pct}% +
`; } static createBreakdown(percentage_1, percentage_2, label_1, label_2) { if (percentage_1 == 0 && percentage_2 == 0) { - return `
-
-
` + return `
`; } - let progressBars = ''; - + let bars = ''; if (percentage_1 > 0) { - progressBars += `
${label_1}
`; + bars += `
`; } - if (percentage_2 > 0) { - progressBars += `
${label_2}
`; + bars += `
`; } - return `
${progressBars}
` + const legend_1 = percentage_1 > 0 + ? ` + ${label_1} ${Math.floor(percentage_1)}%` : ''; + const legend_2 = percentage_2 > 0 + ? ` + ${label_2} ${Math.floor(percentage_2)}%` : ''; + + return `
+
${bars}
+
${legend_1}${legend_2}
+
`; } /* Return the number of rows available in a table */ diff --git a/http_src/views/private/clients/white-mode.scss b/http_src/views/private/clients/white-mode.scss index 714c1eb89c..7d69830150 100644 --- a/http_src/views/private/clients/white-mode.scss +++ b/http_src/views/private/clients/white-mode.scss @@ -1,4 +1,5 @@ :root { + font-weight: 500; --sidebar-width: 4.5rem; --footer-height: 4rem; --padding-md-four: 1.5rem; @@ -879,6 +880,7 @@ li>a>.fa-external-link-alt { box-shadow: 0px 1px 22px -12px #607D8B; background-color: #FFFFFF; padding: 25px 35px 20px 30px; + border-radius: 0.5rem; } .widget-box-fix { @@ -1354,4 +1356,153 @@ a.disabled { .dropdown-item.disabled { color: var(--ntop-disabled-text-color) !important; +} + +/* Flatpickr calendar theme */ +.flatpickr-calendar { + background: var(--bg-surface) !important; + border: 1px solid var(--border-color) !important; + border-radius: 8px !important; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12) !important; + font-size: 0.8rem !important; + color: var(--ntop-text-color) !important; +} + +.flatpickr-calendar.arrowTop::before, +.flatpickr-calendar.arrowTop::after { + border-bottom-color: var(--border-color) !important; +} + +.flatpickr-calendar.arrowBottom::before, +.flatpickr-calendar.arrowBottom::after { + border-top-color: var(--border-color) !important; +} + +.flatpickr-months { + background: var(--bg-elevated) !important; + border-bottom: 1px solid var(--border-subtle) !important; + border-radius: 8px 8px 0 0 !important; + padding: 0.1rem 0 !important; +} + +.flatpickr-months .flatpickr-month, +.flatpickr-months .flatpickr-prev-month, +.flatpickr-months .flatpickr-next-month { + color: var(--ntop-text-color) !important; + fill: var(--ntop-text-color) !important; +} + +.flatpickr-months .flatpickr-prev-month:hover svg, +.flatpickr-months .flatpickr-next-month:hover svg { + fill: var(--ntop-orange) !important; +} + +.flatpickr-current-month .flatpickr-monthDropdown-months, +.flatpickr-current-month input.cur-year { + background: transparent !important; + color: var(--ntop-text-color) !important; + font-size: 0.85rem !important; +} + +.flatpickr-weekdays { + background: var(--bg-elevated) !important; + border-bottom: 1px solid var(--border-subtle) !important; +} + +span.flatpickr-weekday { + background: transparent !important; + color: var(--ntop-muted-text-color) !important; + font-size: 0.72rem !important; + text-transform: uppercase !important; + letter-spacing: 0.04em !important; +} + +.flatpickr-day { + color: var(--ntop-text-color) !important; + border-radius: 5px !important; + font-size: 0.8rem !important; +} + +.flatpickr-day:hover, +.flatpickr-day.prevMonthDay:hover, +.flatpickr-day.nextMonthDay:hover { + background: var(--bg-sunken) !important; + border-color: var(--border-color) !important; +} + +.flatpickr-day.selected, +.flatpickr-day.startRange, +.flatpickr-day.endRange { + background: var(--ntop-blue) !important; + border-color: var(--ntop-blue) !important; + color: #fff !important; +} + +.flatpickr-day.selected:hover, +.flatpickr-day.startRange:hover, +.flatpickr-day.endRange:hover { + background: var(--ntop-blue-dark) !important; + border-color: var(--ntop-blue-dark) !important; +} + +.flatpickr-day.today { + border-color: var(--ntop-orange) !important; + color: var(--ntop-orange) !important; +} + +.flatpickr-day.today:hover { + background: rgba(255, 143, 0, 0.1) !important; +} + +.flatpickr-day.prevMonthDay, +.flatpickr-day.nextMonthDay { + color: var(--ntop-muted-text-color) !important; + opacity: 0.45 !important; +} + +.flatpickr-day.disabled, +.flatpickr-day.disabled:hover { + color: var(--ntop-disabled-text-color) !important; +} + +/* ── Flatpickr time picker ── */ +.flatpickr-time { + background: var(--bg-surface) !important; + border-top: 1px solid var(--border-subtle) !important; + border-radius: 0 0 8px 8px !important; +} + +.flatpickr-time input.flatpickr-hour, +.flatpickr-time input.flatpickr-minute, +.flatpickr-time input.flatpickr-second { + background: transparent !important; + color: var(--ntop-text-color) !important; + font-size: 0.85rem !important; + font-weight: 600 !important; +} + +.flatpickr-time input:hover, +.flatpickr-time input:focus { + background: var(--bg-sunken) !important; +} + +.flatpickr-time .flatpickr-time-separator, +.flatpickr-time .flatpickr-am-pm { + color: var(--ntop-muted-text-color) !important; +} + +.flatpickr-time .numInputWrapper span.arrowUp:after { + border-bottom-color: var(--ntop-muted-text-color) !important; +} + +.flatpickr-time .numInputWrapper span.arrowDown:after { + border-top-color: var(--ntop-muted-text-color) !important; +} + +.flatpickr-innerContainer { + border-bottom: none !important; +} + +.flatpickr-rContainer { + padding: 0.25rem 0 !important; } \ No newline at end of file diff --git a/http_src/vue/chatbot.vue b/http_src/vue/chatbot.vue index 2d911abbbe..334ee86d1c 100644 --- a/http_src/vue/chatbot.vue +++ b/http_src/vue/chatbot.vue @@ -106,6 +106,15 @@ + + + + +
@@ -461,6 +470,7 @@ const providerDropdownOpen = ref(false); const providerSelectorRef = ref(null); const settingsUrl = ref(`${http_prefix}/lua/admin/prefs.lua?tab=llm_providers`); +const statsUrl = ref(`${http_prefix}/lua/pro/ai_stats.lua`); const MAX_HISTORY = 40; const timeoutSec = 120; @@ -499,7 +509,7 @@ function getProviderIcon(provider) { // Add messagem when it arrives or user writes a new message function pushMessage(role, content, error = false, stats = null, artifact = null, queries = null) { messages.value.push({ role, content, time: nowTime(), error, stats, artifact, queries }); - nextTick(scrollBottom); + nextTick(role === 'assistant' ? scrollToLastMessage : scrollBottom); } // scroll view to bottom @@ -509,6 +519,15 @@ function scrollBottom() { } } +function scrollToLastMessage() { + if (!messageList.value) return; + const bubbles = messageList.value.querySelectorAll('.chat-bubble'); + const last = bubbles[bubbles.length - 1]; + if (last) { + last.scrollIntoView({ block: 'start', behavior: 'smooth' }); + } +} + function autoResize(e) { const el = e.target; el.style.height = "auto"; @@ -767,12 +786,12 @@ async function send() { headers: { "Content-Type": "application/json" }, body, signal: controller.signal, - }, /* throw_exception */ true); + }, /* throw_exception */ true, /* not_unwrap */ false, /* return_error */ true); clearTimeout(timer); const reply = rsp?.reply ?? null; - if (!reply) throw new Error(_i18n("llm.empty_response_error")); + if (!reply) throw new Error(rsp?.error_message ?? _i18n("llm.generic_error")); // Append assistant message to history so next request includes it history.value.push({ role: "assistant", content: reply }); @@ -788,7 +807,7 @@ async function send() { if (err.name === "AbortError") { pushMessage("assistant", _i18n("llm.timeout_error_message"), true); } else { - pushMessage("assistant", `${_i18n("llm.request_error")}: ${err.message}`, true); + pushMessage("assistant", err.message || _i18n("llm.generic_error"), true); } } finally { sending.value = false; @@ -798,9 +817,21 @@ async function send() { // On component mount load providers and chat history onMounted(() => { + // if a chatId is selected in the url, pass it to retrieve the selected chat + const selected_chatId = ntopng_url_manager.get_url_entry("chatId"); + + if (selected_chatId) { + chat_UUID.value = selected_chatId; + loadChat(chat_UUID.value); + } else { + // at page load always leave historical chat sidebar open if no chat ID is selected + sidebarOpen.value = true; + } + loadProviders(); loadChatHistory(); document.addEventListener('click', onDocumentClick); + }); onBeforeUnmount(() => { diff --git a/http_src/vue/dashboard-lateral-pie.vue b/http_src/vue/dashboard-lateral-pie.vue index a2e3cffa0f..28b334432b 100644 --- a/http_src/vue/dashboard-lateral-pie.vue +++ b/http_src/vue/dashboard-lateral-pie.vue @@ -75,7 +75,6 @@ async function get_chart_data() { } function drawChart(data) { - debugger; const container = chartContainer.value; // Check that container exists before proceeding @@ -358,7 +357,6 @@ async function refresh_chart() { isLoading.value = (props?.showOnlyFirstLoading === true) ? (firstLoading.value && true) : true; const data = await get_chart_data(); - debugger; if (!data) { chart_data_available.value = false; isLoading.value = false diff --git a/http_src/vue/date-time-range-picker.vue b/http_src/vue/date-time-range-picker.vue index dbfd425ec3..1a0c172972 100644 --- a/http_src/vue/date-time-range-picker.vue +++ b/http_src/vue/date-time-range-picker.vue @@ -1,53 +1,54 @@ @@ -252,19 +253,6 @@ export default { }); }, apply: function () { - // let date_begin = this.$refs["begin-date"].valueAsDate; - // let d_time_begin = this.$refs["begin-time"].valueAsDate; - // date_begin.setHours(d_time_begin.getHours()); - // date_begin.setMinutes(d_time_begin.getMinutes() + d_time_begin.getTimezoneOffset()); - // date_begin.setSeconds(d_time_begin.getSeconds()); - - // let date_end = this.$refs["end-date"].valueAsDate; - // let d_time_end = this.$refs["end-time"].valueAsDate; - // date_end.setHours(d_time_end.getHours()); - // date_end.setMinutes(d_time_end.getMinutes() + d_time_end.getTimezoneOffset()); - // date_end.setSeconds(d_time_end.getSeconds()); - // let epoch_begin = this.get_utc_seconds(date_begin.valueOf()); - // let epoch_end = this.get_utc_seconds(date_end.valueOf()); let now_s = this.get_utc_seconds(Date.now()); let begin_date = FormatterUtils.server_date_to_date(this.flat_begin_date.selectedDates[0]); let epoch_begin = this.get_utc_seconds(begin_date.getTime()); @@ -276,16 +264,6 @@ export default { let status = { epoch_begin, epoch_end }; this.emit_epoch_change(status); }, - // set_date_time: function(ref_name, utc_ts, is_time) { - // utc_ts = this.get_utc_seconds(utc_ts) * 1000; - // let date_time = new Date(utc_ts); - // date_time.setMinutes(date_time.getMinutes() - date_time.getTimezoneOffset()); - // if (is_time) { - // this.$refs[ref_name].value = date_time.toISOString().substring(11,16); - // } else { - // this.$refs[ref_name].value = date_time.toISOString().substring(0,10); - // } - // }, change_select_time: function (refresh_data) { let epoch_end; let epoch_begin; @@ -433,9 +411,107 @@ export default { diff --git a/http_src/vue/ntop_vue.js b/http_src/vue/ntop_vue.js index ce8ad45222..7c45be897c 100644 --- a/http_src/vue/ntop_vue.js +++ b/http_src/vue/ntop_vue.js @@ -107,6 +107,8 @@ import { default as PageDefsOverview } from "./page-defs-overview.vue" import { default as PageChecksOverview } from "./page-checks-overview.vue" import { default as PageManageData } from "./page-manage-data.vue" import { default as PageEditDeviceProtocols } from "./page-edit-device-protocols.vue" +import { default as PageAiStats } from "./page-ai-stats.vue" +//import { default as PageInternals } from "./page-internals.vue" /* Testing page */ import { default as PageTest } from "./page-test.vue"; @@ -251,6 +253,8 @@ let ntopVue = { PageExporterMap: PageExporterMap, PageManageData: PageManageData, PageEditDeviceProtocols: PageEditDeviceProtocols, + PageAiStats: PageAiStats, + //PageInternals: PageInternals, PageExportersGraph: PageExportersGraph, PageAbout: PageAbout, diff --git a/http_src/vue/page-ai-stats.vue b/http_src/vue/page-ai-stats.vue new file mode 100644 index 0000000000..0054b14cc0 --- /dev/null +++ b/http_src/vue/page-ai-stats.vue @@ -0,0 +1,651 @@ + + + + + diff --git a/http_src/vue/page-ports-sankey.vue b/http_src/vue/page-ports-sankey.vue index 335009843d..ee3088176e 100644 --- a/http_src/vue/page-ports-sankey.vue +++ b/http_src/vue/page-ports-sankey.vue @@ -92,7 +92,6 @@ const updateSankeyData = async () => { /* ************************************** */ const changedOption = (opt) => { - debugger ntopng_url_manager.set_key_to_url(opt.filter_name, opt.id) updateSankeyData(); } diff --git a/http_src/vue/page-snmp-devices.vue b/http_src/vue/page-snmp-devices.vue index 6792bfe18a..678bf534ba 100644 --- a/http_src/vue/page-snmp-devices.vue +++ b/http_src/vue/page-snmp-devices.vue @@ -431,7 +431,6 @@ function click_button_edit(event) { function click_button_timeseries(event) { const row = event.row; - debugger; window.location.href = `${linksUtils.getSNMPDetailsPageURL(row.column_ip, http_prefix)}&page=historical` } diff --git a/http_src/vue/range-picker.vue b/http_src/vue/range-picker.vue index bffec81c02..3d3e8c05ac 100644 --- a/http_src/vue/range-picker.vue +++ b/http_src/vue/range-picker.vue @@ -7,19 +7,29 @@