diff --git a/ProcessMaker/Http/Controllers/Admin/LogsController.php b/ProcessMaker/Http/Controllers/Admin/LogsController.php new file mode 100644 index 0000000000..37ef170c8c --- /dev/null +++ b/ProcessMaker/Http/Controllers/Admin/LogsController.php @@ -0,0 +1,20 @@ +path($file->id . '/' . $file->file_name); - // Register the Event + // Inline preview when requested + if (request()->boolean('inline', false)) { + if (!empty($file->file_name)) { + FilesDownloaded::dispatch($file); + } + + return response()->file($path, [ + 'Content-Type' => $file->mime_type, + 'Content-Disposition' => 'inline; filename="' . addslashes($file->file_name) . '"', + ]); + } + + // Default: force download if (!empty($file->file_name)) { FilesDownloaded::dispatch($file); } diff --git a/ProcessMaker/Http/Middleware/GenerateMenus.php b/ProcessMaker/Http/Middleware/GenerateMenus.php index f714b52ac5..4ffa3153e6 100644 --- a/ProcessMaker/Http/Middleware/GenerateMenus.php +++ b/ProcessMaker/Http/Middleware/GenerateMenus.php @@ -128,6 +128,14 @@ public function handle(Request $request, Closure $next) 'file' => "data:image/svg+xml;base64,{$devlinkIcon}", ]); } + if (\Auth::user()->canAny('view-settings|edit-settings') && + (hasPackage('package-email-start-event') || hasPackage('package-ai'))) { + $submenu->add(__('Logs'), [ + 'route' => 'admin.logs', + 'icon' => 'fa-bars', + 'id' => 'admin-logs', + ]); + } }); Menu::make('sidebar_task', function ($menu) { $submenu = $menu->add(__('Tasks')); diff --git a/ProcessMaker/ImportExport/Exporters/MediaExporter.php b/ProcessMaker/ImportExport/Exporters/MediaExporter.php index 91c9d62b24..aee92aced0 100644 --- a/ProcessMaker/ImportExport/Exporters/MediaExporter.php +++ b/ProcessMaker/ImportExport/Exporters/MediaExporter.php @@ -44,10 +44,22 @@ public function import(): bool $ref = $this->getReference(DependentType::MEDIA); if ($ref && isset($ref['base64'])) { - $this->model->model->addMediaFromBase64($ref['base64']) + $newMedia = $this->model->model->addMediaFromBase64($ref['base64']) ->usingFileName($this->model->file_name) ->withCustomProperties($this->model->custom_properties) ->toMediaCollection($this->model->collection_name); + + if (hasPackage('package-ai')) { + $updaterClass = 'ProcessMaker\\Package\\PackageAi\\Services\\FlowGenieMediaConfigUpdater'; + if (class_exists($updaterClass)) { + $updaterClass::updateForMediaImport( + (string) $this->model->model_type, + (int) $this->model->model_id, + (int) $this->model->id, + (int) $newMedia->id + ); + } + } } // We should delete the model, because the Spatie library recreates it. diff --git a/database/migrations/2025_04_08_115507_add_the_comments_field_to_the_process_request_token_table.php b/database/migrations/2025_04_08_115507_add_the_comments_field_to_the_process_request_token_table.php index 69811599b1..4877eec6d9 100644 --- a/database/migrations/2025_04_08_115507_add_the_comments_field_to_the_process_request_token_table.php +++ b/database/migrations/2025_04_08_115507_add_the_comments_field_to_the_process_request_token_table.php @@ -4,8 +4,7 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; -return new class extends Migration -{ +return new class extends Migration { /** * Run the migrations. */ diff --git a/resources/js/admin/logs/components/Logs/AgentSessionDetail/AgentSessionDetail.vue b/resources/js/admin/logs/components/Logs/AgentSessionDetail/AgentSessionDetail.vue new file mode 100644 index 0000000000..d8a45bbd3c --- /dev/null +++ b/resources/js/admin/logs/components/Logs/AgentSessionDetail/AgentSessionDetail.vue @@ -0,0 +1,929 @@ + + + + + + diff --git a/resources/js/admin/logs/components/Logs/AgentSessionDetail/index.js b/resources/js/admin/logs/components/Logs/AgentSessionDetail/index.js new file mode 100644 index 0000000000..01a900019c --- /dev/null +++ b/resources/js/admin/logs/components/Logs/AgentSessionDetail/index.js @@ -0,0 +1,5 @@ +import AgentSessionDetail from "./AgentSessionDetail.vue"; + +export { AgentSessionDetail }; +export default AgentSessionDetail; + diff --git a/resources/js/admin/logs/components/Logs/BaseTable/BaseTable.vue b/resources/js/admin/logs/components/Logs/BaseTable/BaseTable.vue new file mode 100644 index 0000000000..da4b99b837 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/BaseTable/BaseTable.vue @@ -0,0 +1,87 @@ + + + diff --git a/resources/js/admin/logs/components/Logs/BaseTable/index.js b/resources/js/admin/logs/components/Logs/BaseTable/index.js new file mode 100644 index 0000000000..f8430fb194 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/BaseTable/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as BaseTable } from './BaseTable.vue'; + diff --git a/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue b/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue new file mode 100644 index 0000000000..6878c1c6fc --- /dev/null +++ b/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue @@ -0,0 +1,148 @@ + + + + diff --git a/resources/js/admin/logs/components/Logs/HeaderBar/index.js b/resources/js/admin/logs/components/Logs/HeaderBar/index.js new file mode 100644 index 0000000000..c2176bf01c --- /dev/null +++ b/resources/js/admin/logs/components/Logs/HeaderBar/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as HeaderBar } from './HeaderBar.vue'; + diff --git a/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue b/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue new file mode 100644 index 0000000000..9825c0a42e --- /dev/null +++ b/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue @@ -0,0 +1,159 @@ + + + + + diff --git a/resources/js/admin/logs/components/Logs/LogContainer/index.js b/resources/js/admin/logs/components/Logs/LogContainer/index.js new file mode 100644 index 0000000000..d0ad929b7e --- /dev/null +++ b/resources/js/admin/logs/components/Logs/LogContainer/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as LogContainer } from './LogContainer.vue'; + diff --git a/resources/js/admin/logs/components/Logs/LogTable/LogTable.vue b/resources/js/admin/logs/components/Logs/LogTable/LogTable.vue new file mode 100644 index 0000000000..690552e324 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/LogTable/LogTable.vue @@ -0,0 +1,317 @@ + + + + diff --git a/resources/js/admin/logs/components/Logs/LogTable/index.js b/resources/js/admin/logs/components/Logs/LogTable/index.js new file mode 100644 index 0000000000..d07b1072e7 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/LogTable/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as LogTable } from './LogTable.vue'; + diff --git a/resources/js/admin/logs/components/Logs/Pagination/Pagination.vue b/resources/js/admin/logs/components/Logs/Pagination/Pagination.vue new file mode 100644 index 0000000000..33b47377b4 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/Pagination/Pagination.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/resources/js/admin/logs/components/Logs/Pagination/index.js b/resources/js/admin/logs/components/Logs/Pagination/index.js new file mode 100644 index 0000000000..d2d0ae6aa6 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/Pagination/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as Pagination } from './Pagination.vue'; + diff --git a/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue b/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue new file mode 100644 index 0000000000..a2276bbff7 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue @@ -0,0 +1,196 @@ + + + diff --git a/resources/js/admin/logs/components/Logs/Sidebar/index.js b/resources/js/admin/logs/components/Logs/Sidebar/index.js new file mode 100644 index 0000000000..a4f8b5ce94 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/Sidebar/index.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as Sidebar } from './Sidebar.vue'; + diff --git a/resources/js/admin/logs/components/Logs/routes.js b/resources/js/admin/logs/components/Logs/routes.js new file mode 100644 index 0000000000..398d462193 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/routes.js @@ -0,0 +1,92 @@ +import { LogTable } from "./LogTable"; + +export default {}; + +/** + * Check if a package is installed + * @param {string} packageName - The package name to check + * @returns {boolean} + */ +const isPackageInstalled = (packageName) => window.ProcessMaker?.packages?.includes(packageName); + +/** + * Check if email start event package is installed + * @returns {boolean} + */ +export const hasEmailPackage = () => isPackageInstalled("package-email-start-event"); + +/** + * Check if AI package is installed + * @returns {boolean} + */ +export const hasAiPackage = () => isPackageInstalled("package-ai"); + +/** + * Determine the default redirect path based on installed packages + * @returns {string} + */ +const getDefaultRedirectPath = () => { + if (hasEmailPackage()) { + return "/email/errors"; + } + if (hasAiPackage()) { + return "/agents/design"; + } + // Fallback - shouldn't happen if menu visibility is correct + return "/email/errors"; +}; + +export const routes = [ + { + name: "logs.index", + path: "/", + beforeEnter: (to, from, next) => { + next(getDefaultRedirectPath()); + }, + }, + // Email logs routes + { + name: "logs.email", + path: "/email/:logType", + component: LogTable, + props(route) { + return { + category: "email", + logType: route.params.logType, + }; + }, + beforeEnter: (to, from, next) => { + if (!hasEmailPackage()) { + // Redirect to agents if email package not installed + next(hasAiPackage() ? "/agents/design" : "/"); + } else { + next(); + } + }, + }, + // FlowGenie Agents logs routes + { + name: "logs.agents.redirect", + path: "/agents", + redirect: "/agents/design", + }, + { + name: "logs.agents", + path: "/agents/:logType", + component: LogTable, + props(route) { + return { + category: "agents", + logType: route.params.logType, + }; + }, + beforeEnter: (to, from, next) => { + if (!hasAiPackage()) { + // Redirect to email if AI package not installed + next(hasEmailPackage() ? "/email/errors" : "/"); + } else { + next(); + } + }, + }, +]; diff --git a/resources/js/admin/logs/index.js b/resources/js/admin/logs/index.js new file mode 100644 index 0000000000..bdf9dc220b --- /dev/null +++ b/resources/js/admin/logs/index.js @@ -0,0 +1,26 @@ +import { LogContainer } from './components/Logs/LogContainer'; +import { routes } from './components/Logs/routes'; + +// eslint-disable-next-line no-undef +Vue.use(VueRouter); + +// eslint-disable-next-line no-undef +const router = new VueRouter({ + mode: 'history', + base: '/admin/logs', + routes, +}); + +window.Vue.component('admin-logs', LogContainer); + +document.addEventListener('DOMContentLoaded', () => { + new window.Vue({ + el: '#admin-logs-main', + router, + components: { + LogContainer, + }, + render: (h) => h(LogContainer), + }); +}); + diff --git a/resources/js/admin/logs/utils/date.js b/resources/js/admin/logs/utils/date.js new file mode 100644 index 0000000000..491fa1a8d8 --- /dev/null +++ b/resources/js/admin/logs/utils/date.js @@ -0,0 +1,74 @@ +/* eslint-disable import/prefer-default-export */ +import { DateTime } from 'luxon'; + +/** + * Convert PHP/moment date format to Luxon format + * @param {string} format - PHP/moment format string + * @returns {string} - Luxon format string + */ +const convertToLuxonFormat = (format) => { + // Common conversions from PHP/moment to Luxon + const replacements = { + YYYY: 'yyyy', + YY: 'yy', + MM: 'LL', + M: 'L', + DD: 'dd', + D: 'd', + HH: 'HH', + hh: 'hh', + H: 'H', + h: 'h', + mm: 'mm', + m: 'm', + ss: 'ss', + s: 's', + A: 'a', + a: 'a', + }; + + let luxonFormat = format; + Object.entries(replacements).forEach(([from, to]) => { + luxonFormat = luxonFormat.replace(new RegExp(from, 'g'), to); + }); + + return luxonFormat; +}; + +/** + * Format date to user's date format + * @param {string} value - The date to format + * @returns {string} - The formatted date + */ +export const dateFormatter = (value) => { + let datetimeConfig = 'dd/LL/yyyy hh:mm'; + let timezoneConfig = 'UTC'; + + if ( + typeof ProcessMaker !== 'undefined' + && ProcessMaker.user + && ProcessMaker.user.datetime_format + ) { + timezoneConfig = ProcessMaker.user.timezone; + datetimeConfig = convertToLuxonFormat(ProcessMaker.user.datetime_format); + } + + if (value) { + const date = DateTime.fromISO(value, { zone: 'utc' }).setZone(timezoneConfig); + + if (date.isValid) { + return date.toFormat(datetimeConfig); + } + + // Try parsing as SQL format + const sqlDate = DateTime.fromSQL(value, { zone: 'utc' }).setZone(timezoneConfig); + if (sqlDate.isValid) { + return sqlDate.toFormat(datetimeConfig); + } + + return value; + } + + return '-'; +}; + diff --git a/resources/lang/en.json b/resources/lang/en.json index e580e5b49b..5e07c388e7 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -656,6 +656,7 @@ "Description": "Description", "Design Screen": "Design Screen", "Design": "Design", + "Design Mode Logs": "Design Mode Logs", "Designer": "Designer", "Destination Screen": "Destination Screen", "Destination": "Destination", @@ -899,6 +900,7 @@ "Error Name": "Error Name", "Error": "Error", "Errors": "Errors", + "Error Logs": "Error Logs", "Event Based Gateway": "Event Based Gateway", "Event has multiple event definitions": "Event has multiple event definitions", "Event-based Gateway": "Event-based Gateway", @@ -906,6 +908,8 @@ "Event": "Event", "Exclusive Gateway": "Exclusive Gateway", "Execution Error": "Execution Error", + "Runtime Logs": "Runtime Logs", + "FlowGenie Studio Logs": "FlowGenie Studio Logs", "Execution Log": "Execution Log", "Executor Successfully Built. You can now close this window. ": "Executor Successfully Built. You can now close this window. ", "Existing Array": "Existing Array", @@ -979,6 +983,7 @@ "Flow splits implicitly": "Flow splits implicitly", "Flow Variable": "Flow Variable", "FlowGenies": "FlowGenies", + "FlowGenie Agents Logs": "FlowGenie Agents Logs", "FolderCreated": "Folder Created", "FolderUpdated": "Folder Updated", "Font Size": "Font Size", @@ -1255,6 +1260,7 @@ "Manually Complete Request": "Manually Complete Request", "Manually reconfigure the dashboard using an existing one in the environment.": "Manually reconfigure the dashboard using an existing one in the environment.", "Mark as Priority": "Mark as Priority", + "Matched Logs": "Matched Logs", "Max Input": "Max Input", "Max Length": "Max Length", "Max": "Max", @@ -2407,6 +2413,7 @@ "Top Right": "Top Right", "Top": "Top", "Total Elements": "Total Elements", + "Total Logs": "Total Logs", "Translate into multiple languages.": "Translate into multiple languages.", "Translate process screens to a desired language": "Translate process screens to a desired language", "Translation in progress": "Translation in progress", diff --git a/resources/views/admin/logs/index.blade.php b/resources/views/admin/logs/index.blade.php new file mode 100644 index 0000000000..9a7ca90a61 --- /dev/null +++ b/resources/views/admin/logs/index.blade.php @@ -0,0 +1,28 @@ +@extends('layouts.layout') + +@section('title') + {{__('Logs')}} +@endsection + +@section('sidebar') + @include('layouts.sidebar', ['sidebar'=> Menu::get('sidebar_admin')]) +@endsection + +@section('breadcrumbs') + @include('shared.breadcrumbs', ['routes' => [ + __('Admin') => route('admin.index'), + __('Logs') => null, + ]]) +@endsection + +@section('content') +
+@endsection + +@section('js') + + +@endsection + diff --git a/resources/views/layouts/layout.blade.php b/resources/views/layouts/layout.blade.php index 9843813b0f..253d4343c4 100644 --- a/resources/views/layouts/layout.blade.php +++ b/resources/views/layouts/layout.blade.php @@ -141,7 +141,8 @@ class="main flex-grow-1 h-100 window.ProcessMaker.packages = @json(\App::make(ProcessMaker\Managers\PackageManager::class)->listPackages()); window.ProcessMaker.ai = { ragCollections: @json(config('ai.rag_collections.enabled')), - genieClientTimeout: @json(config('ai.genie_client.timeout')) + genieClientTimeout: @json(config('ai.genie_client.timeout')), + microserviceHost: @json(config('app.ai_microservice_host')) }; diff --git a/routes/web.php b/routes/web.php index b7bf10904f..69abeddef1 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,5 +1,6 @@ name('storage.serve'); Route::middleware('auth', 'session_kill', 'sanitize', 'force_change_password', '2fa')->group(function () { + Route::get('files/{file}/contents', [ApiFileController::class, 'download']) + ->name('web.files.download') + ->middleware('can:view,file'); + // Routes related to Authentication (password reset, etc) // Auth::routes(); Route::prefix('admin')->group(function () { @@ -81,6 +88,18 @@ // temporary, should be removed Route::get('security-logs/download/all', [ProcessMaker\Http\Controllers\Api\SecurityLogController::class, 'downloadForAllUsers'])->middleware('can:view-security-logs'); Route::get('security-logs/download/{user}', [ProcessMaker\Http\Controllers\Api\SecurityLogController::class, 'downloadForUser'])->middleware('can:view-security-logs'); + + // Logs - available when package-email-start-event or package-ai is installed + if (hasPackage('package-email-start-event') || hasPackage('package-ai')) { + Route::get('logs', [LogsController::class, 'index'])->name('admin.logs')->middleware('can:view-settings'); + // Export route must be before the wildcard route + if (hasPackage('package-email-start-event')) { + Route::get('logs/export/csv', [ProcessMaker\Package\PackageEmailStartEvent\Http\Controllers\EmailListenerLogController::class, 'exportToCsv']) + ->name('admin.logs.export.csv') + ->middleware('can:view-settings'); + } + Route::get('logs/{any}', [LogsController::class, 'index'])->name('admin.logs-any')->middleware('can:view-settings')->where('any', '.*'); + } }); Route::get('admin', [AdminController::class, 'index'])->name('admin.index'); diff --git a/webpack.mix.js b/webpack.mix.js index 01253ace95..c93f125c10 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -104,6 +104,7 @@ mix .js("resources/js/admin/cssOverride/edit.js", "public/js/admin/cssOverride/edit.js") .js("resources/js/admin/script-executors/index.js", "public/js/admin/script-executors/index.js") .js("resources/js/admin/tenant-queues/index.js", "public/js/admin/tenant-queues/index.js") + .js("resources/js/admin/logs/index.js", "public/js/admin/logs/index.js") .js("resources/js/processes/index.js", "public/js/processes") .js("resources/js/processes/edit.js", "public/js/processes")