Updated Logging System to Include Application Logs

This commit is contained in:
2026-06-08 12:25:47 -05:00
parent 15e93c1e45
commit 9a9f890494
13 changed files with 323 additions and 12 deletions

View File

@@ -540,6 +540,7 @@ export default {
'admin-users': 'User accounts, teams, and role permissions',
'admin-security': 'Password rules, account lockout, and complexity policy',
'admin-audit': 'Complete audit trail of all user activity, with export',
'admin-logs': 'Application/event log — HTTP, SMTP, webhook, and Teams activity',
'admin-config': 'Application settings, maintenance defaults, and tax rule sets',
'admin-integrations': 'Email, webhooks, ServiceNow, and Workday connectors'
}[this.activeView];
@@ -563,6 +564,7 @@ export default {
'admin-users': 'Users & Teams',
'admin-security': 'Password & Security',
'admin-audit': 'Activity & Audit Trail',
'admin-logs': 'Application Logs',
'admin-config': 'Application Configuration',
'admin-integrations': 'Integrations'
}[this.activeView];
@@ -627,6 +629,7 @@ export default {
'admin-users': ['admin.users'],
'admin-security': ['admin.security'],
'admin-audit': ['admin.audit'],
'admin-logs': ['admin.logs'],
'admin-config': ['admin.settings', 'config.manage'],
'admin-integrations': ['admin.integrations']
};

View File

@@ -0,0 +1,151 @@
<template>
<v-card class="span-12 panel-pad">
<div class="toolbar-row mb-3">
<div>
<h2 class="section-title">Application logs</h2>
<p class="quiet text-body-2">
Operational/event log of the application HTTP requests and outbound integration activity (SMTP, webhook, Teams,
ServiceNow, Workday). Separate from <strong>Activity &amp; Audit Trail</strong>, which records user data changes.
</p>
</div>
<v-spacer />
<v-btn variant="tonal" prepend-icon="mdi-refresh" :loading="loading" @click="load">Refresh</v-btn>
</div>
<div class="filter-grid mb-3">
<v-select v-model="filters.category" :items="categories" label="Category" density="compact" hide-details clearable @update:model-value="load" />
<v-select v-model="filters.level" :items="levels" label="Level" density="compact" hide-details clearable @update:model-value="load" />
<v-text-field
v-model="filters.q"
label="Search"
placeholder="message or detail…"
density="compact"
hide-details
clearable
prepend-inner-icon="mdi-magnify"
@keyup.enter="load"
@click:clear="load"
/>
</div>
<div class="toolbar-row mb-3">
<v-btn size="small" color="primary" variant="tonal" prepend-icon="mdi-filter" @click="load">Apply</v-btn>
<v-spacer />
<span class="quiet text-body-2">{{ total }} matching event{{ total === 1 ? '' : 's' }} (showing {{ logs.length }})</span>
</div>
<v-alert v-if="error" type="error" density="compact" class="mb-3">{{ error }}</v-alert>
<v-table density="compact" class="log-table">
<thead>
<tr><th>Time</th><th>Level</th><th>Category</th><th>Message</th><th>Detail</th></tr>
</thead>
<tbody>
<tr v-for="(row, i) in logs" :key="i">
<td class="nowrap">{{ shortDate(row.timestamp) }}</td>
<td><v-chip size="x-small" :color="levelColor(row.level)" variant="tonal">{{ row.level }}</v-chip></td>
<td><v-chip size="x-small" variant="tonal">{{ row.category }}</v-chip></td>
<td>{{ row.message }}</td>
<td>
<v-menu v-if="row.meta && Object.keys(row.meta).length" location="start">
<template #activator="{ props }">
<v-btn v-bind="props" size="x-small" variant="text" icon="mdi-information-outline" />
</template>
<v-card class="detail-card pa-3">
<pre class="detail-json">{{ pretty(row.meta) }}</pre>
</v-card>
</v-menu>
<span v-else class="quiet"></span>
</td>
</tr>
<tr v-if="!logs.length && !loading">
<td colspan="5" class="text-center quiet py-4">No log events match these filters.</td>
</tr>
</tbody>
</v-table>
</v-card>
</template>
<script>
import { apiRequest } from '../services/api';
import { shortDate } from '../utils/format';
// Application Logs viewer (Admin → Application Logs). Reads the Winston event log file via
// /api/app-logs (filtered by category/level/search); the category/level dropdowns come from the facets
// the endpoint returns. Distinct from the DB-backed Activity & Audit Trail.
export default {
props: {
token: { type: String, required: true }
},
data() {
return {
filters: { category: null, level: null, q: '' },
logs: [],
categories: [],
levels: [],
total: 0,
loading: false,
error: ''
};
},
mounted() {
this.load();
},
methods: {
shortDate,
async api(path, options = {}) {
return apiRequest(path, this.token, options);
},
async load() {
this.loading = true;
this.error = '';
try {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(this.filters)) {
if (value !== null && value !== '' && value !== undefined) params.set(key, value);
}
const data = await (await this.api(`/api/app-logs?${params.toString()}`)).json();
this.logs = data.logs;
this.total = data.total;
this.categories = data.categories;
this.levels = data.levels;
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
},
levelColor(level) {
return { error: 'error', warn: 'warning', info: 'info', debug: 'grey' }[level] || undefined;
},
pretty(meta) {
try {
return JSON.stringify(meta, null, 2);
} catch {
return String(meta);
}
}
}
};
</script>
<style scoped>
.filter-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.log-table .nowrap {
white-space: nowrap;
}
.detail-card {
max-width: 520px;
max-height: 60vh;
overflow: auto;
}
.detail-json {
font-size: 11px;
white-space: pre-wrap;
word-break: break-word;
margin: 0;
}
</style>

View File

@@ -173,6 +173,7 @@ export const navItems = [
{ key: 'admin-users', label: 'Users & Teams', icon: 'mdi-account-group-outline' },
{ key: 'admin-security', label: 'Password & Security', icon: 'mdi-shield-key-outline' },
{ key: 'admin-audit', label: 'Activity & Audit Trail', icon: 'mdi-history' },
{ key: 'admin-logs', label: 'Application Logs', icon: 'mdi-text-box-search-outline' },
{ key: 'admin-config', label: 'Application Configuration', icon: 'mdi-tune' },
{ key: 'admin-integrations', label: 'Integrations', icon: 'mdi-transit-connection-variant' }
]

View File

@@ -170,6 +170,11 @@
<AuditTrailSettings :token="token" />
</template>
<!-- APPLICATION LOGS -->
<template v-if="adminSection === 'admin-logs'">
<AppLogsSettings :token="token" />
</template>
<!-- APPLICATION CONFIGURATION -->
<template v-if="adminSection === 'admin-config'">
<CompanySettings :token="token" @updated="$emit('companies-updated')" />
@@ -309,6 +314,7 @@
<script>
import AssetClassSettings from '../components/AssetClassSettings.vue';
import AssetIdTemplateSettings from '../components/AssetIdTemplateSettings.vue';
import AppLogsSettings from '../components/AppLogsSettings.vue';
import AuditTrailSettings from '../components/AuditTrailSettings.vue';
import CategorySettings from '../components/CategorySettings.vue';
import CompanySettings from '../components/CompanySettings.vue';
@@ -326,7 +332,7 @@ import TeamsSettings from '../components/TeamsSettings.vue';
import WebhookSettings from '../components/WebhookSettings.vue';
export default {
components: { AssetClassSettings, AssetIdTemplateSettings, AuditTrailSettings, CategorySettings, CompanySettings, DataTable, DepartmentSettings, DepreciationZoneSettings, NotificationSettings, PartCategorySettings, PasswordPolicySettings, PmCategorySettings, PmSettings, Section179LimitSettings, ServiceNowSettings, TeamsSettings, WebhookSettings },
components: { AppLogsSettings, AssetClassSettings, AssetIdTemplateSettings, AuditTrailSettings, CategorySettings, CompanySettings, DataTable, DepartmentSettings, DepreciationZoneSettings, NotificationSettings, PartCategorySettings, PasswordPolicySettings, PmCategorySettings, PmSettings, Section179LimitSettings, ServiceNowSettings, TeamsSettings, WebhookSettings },
props: {
token: { type: String, default: '' },
section: { type: String, default: 'admin-users' },
@@ -397,7 +403,7 @@ export default {
return groups;
},
adminSection() {
return ['admin-users', 'admin-security', 'admin-audit', 'admin-config', 'admin-integrations'].includes(this.section) ? this.section : 'admin-users';
return ['admin-users', 'admin-security', 'admin-audit', 'admin-logs', 'admin-config', 'admin-integrations'].includes(this.section) ? this.section : 'admin-users';
}
},
watch: {