Implement Activity Log management and Person activity integration

- Updated qry__activity_log to support filtering by person_id
- Created /core/activity_logs standalone page for monitoring system actions
- Enhanced Person detail page with 'Recent Activity' column showing real data
- Added 'Activity Logs' card to the Core Management dashboard
This commit is contained in:
Scott Idem
2026-01-07 12:20:52 -05:00
parent 5bd7c4756c
commit 07479f17a8
5 changed files with 307 additions and 12 deletions

View File

@@ -181,52 +181,112 @@ export async function update_ae_obj__activity_log({
return ae_promises.update__activity_log_obj;
}
// Updated 2026-01-06
// Updated 2026-01-07
export async function qry__activity_log({
api_cfg,
account_id,
qry_str,
qry_person_id = null,
enabled = 'enabled',
hidden = 'not_hidden',
view = 'default',
limit = 50,
offset = 0,
order_by_li = { created_on: 'DESC' },
log_lvl = 0
}: {
api_cfg: any;
account_id: string;
qry_str?: string;
qry_person_id?: string | null;
enabled?: 'enabled' | 'all' | 'not_enabled';
hidden?: 'hidden' | 'all' | 'not_hidden';
view?: string;
limit?: number;
offset?: number;
order_by_li?: Record<string, 'ASC' | 'DESC'>;
log_lvl?: number;
}) {
const search_query: any = { and: [] };
if (account_id) {
search_query.and.push({ field: 'account_id_random', op: 'eq', value: account_id });
}
if (qry_str) {
search_query.q = qry_str;
}
if (qry_person_id) {
search_query.and.push({ field: 'person_id_random', op: 'eq', value: qry_person_id });
}
ae_promises.load__activity_log_obj_li = await api.search_ae_obj_v3({
api_cfg,
obj_type: 'activity_log',
search_query,
enabled,
hidden,
view,
limit,
offset,
order_by_li,
log_lvl
});
return ae_promises.load__activity_log_obj_li;
}
}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { Building, Globe, Users, ShieldCheck, List } from 'lucide-svelte';
import { Building, Globe, Users, ShieldCheck, List, History } from 'lucide-svelte';
import { ae_loc, slct } from '$lib/stores/ae_stores';
interface Props {
@@ -70,6 +70,18 @@
</a>
</div>
<!-- Activity Log Card -->
<div class="card p-4 space-y-4 variant-soft-success">
<div class="flex items-center gap-2">
<History size={20} />
<h3 class="h4">Activity Logs</h3>
</div>
<p class="text-sm opacity-80 text-balance">Monitor system actions and historical changes for the account.</p>
<a class="btn variant-filled-success w-full" href="/core/activity_logs">
View Activity Logs
</a>
</div>
<!-- Lookups Card -->
<div class="card p-4 space-y-4 variant-soft-surface">
<div class="flex items-center gap-2">

View File

@@ -0,0 +1,179 @@
<script lang="ts">
/** @type {import('./$types').PageData} */
import { onMount } from 'svelte';
import { qry__activity_log } from '$lib/ae_core/core__activity_log';
import { ae_api, ae_loc } from '$lib/stores/ae_stores';
import { Search, History, Calendar, User, Tag, Activity } from 'lucide-svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
// State
let log_li: any[] = $state([]);
let loading = $state(true);
let qry_str = $state('');
let limit = $state(50);
/**
* Fetches activity logs from the API based on current filters.
*/
async function load_logs() {
if (!$ae_loc.account_id) return;
loading = true;
const results = await qry__activity_log({
api_cfg: $ae_api,
account_id: $ae_loc.account_id,
qry_str: qry_str || undefined,
limit: limit,
log_lvl: 1
});
log_li = results || [];
loading = false;
}
onMount(() => {
load_logs();
});
/**
* Handles the search form submission.
*/
function handle_search(e: Event) {
e.preventDefault();
load_logs();
}
</script>
<div class="container mx-auto p-4 space-y-4">
<header class="flex justify-between items-center">
<div class="flex items-center gap-2">
<History size={24} />
<h1 class="h2">Activity Logs</h1>
</div>
</header>
<!-- Filter Bar -->
<div class="card p-4 variant-soft">
<form onsubmit={handle_search} class="flex flex-wrap gap-4 items-end">
<label class="label grow max-w-md">
<span>Search (Keyword)</span>
<div class="input-group input-group-divider grid-cols-[1fr_auto]">
<input type="search" placeholder="Action, name, description..." bind:value={qry_str} />
<button type="submit" class="variant-filled-secondary">
<Search size={16} />
</button>
</div>
</label>
<label class="label">
<span>Limit</span>
<select class="select" bind:value={limit} onchange={load_logs}>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={250}>200</option>
</select>
</label>
<button type="button" class="btn variant-filled-secondary" onclick={load_logs} disabled={loading}>
Refresh
</button>
</form>
</div>
{#if loading}
<div class="flex justify-center p-12">
<div class="placeholder animate-pulse w-full h-32"></div>
</div>
{:else if log_li.length === 0}
<div class="card p-8 text-center">
<p>No activity logs found for this account matching your criteria.</p>
</div>
{:else}
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th>Date/Time</th>
<th>User/Person</th>
<th>Action</th>
<th>Context</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
{#each log_li as log}
<tr>
<!-- Date/Time -->
<td class="whitespace-nowrap">
<div class="flex flex-col">
<span class="font-bold">{ae_util.iso_datetime_formatter(log.created_on, 'date_short')}</span>
<span class="text-xs opacity-60">{ae_util.iso_datetime_formatter(log.created_on, 'time_12_short')}</span>
</div>
</td>
<!-- User/Person -->
<td>
<div class="flex flex-col max-w-[200px] truncate">
{#if log.person_full_name || log.person_id_random}
<span class="flex items-center gap-1">
<User size={12} />
{log.person_full_name || 'Person'}
</span>
<span class="text-[10px] opacity-50 font-mono">{log.person_id_random || '--'}</span>
{:else if log.name}
<span class="italic text-xs opacity-70">{log.name}</span>
{:else}
<span class="opacity-30">--</span>
{/if}
</div>
</td>
<!-- Action -->
<td>
<span class="badge variant-soft-primary uppercase text-[10px] tracking-wider">
{log.action}
</span>
{#if log.action_with}
<span class="text-xs opacity-60 ml-1">via {log.action_with}</span>
{/if}
</td>
<!-- Context -->
<td>
{#if log.object_type}
<div class="flex flex-col text-xs">
<span class="flex items-center gap-1 opacity-70">
<Tag size={10} />
{log.object_type}
</span>
{#if log.external_client_id}
<span class="text-[10px] opacity-50 font-mono truncate max-w-[100px]">{log.external_client_id}</span>
{/if}
</div>
{:else}
<span class="opacity-30">--</span>
{/if}
</td>
<!-- Summary/Description -->
<td>
<div class="flex flex-col gap-1 max-w-md">
{#if log.summary}
<span class="font-medium text-sm">{log.summary}</span>
{/if}
{#if log.description}
<p class="text-xs opacity-70 line-clamp-2" title={log.description}>{log.description}</p>
{/if}
{#if !log.summary && !log.description}
<span class="italic opacity-30 text-xs">No detail provided</span>
{/if}
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>

View File

@@ -0,0 +1,5 @@
/** @type {import('./$types').PageLoad} */
export async function load({ parent }) {
const data = await parent();
return data;
}

View File

@@ -31,7 +31,8 @@
import { update_ae_obj__person } from '$lib/ae_core/ae_core__person';
import { qry_ae_obj_li__event } from '$lib/ae_events/ae_events__event';
import { qry__post } from '$lib/ae_posts/ae_posts__post';
import { Users, Link, Unlink, UserPlus, ShieldCheck, User, Calendar, MessageSquare, History } from 'lucide-svelte';
import { qry__activity_log } from '$lib/ae_core/core__activity_log';
import { Users, Link, Unlink, UserPlus, ShieldCheck, User, Calendar, MessageSquare, History, Activity } from 'lucide-svelte';
interface Props {
data: any;
@@ -59,6 +60,7 @@
let related_events: any[] = $state([]);
let related_posts: any[] = $state([]);
let related_activity_logs: any[] = $state([]);
let loading_activity = $state(false);
async function load_activity() {
@@ -66,7 +68,7 @@
loading_activity = true;
// Load related data using search queries
const [events, posts] = await Promise.all([
const [events, posts, logs] = await Promise.all([
qry_ae_obj_li__event({
api_cfg: $ae_api,
for_obj_id: $ae_loc.account_id,
@@ -78,11 +80,19 @@
account_id: $ae_loc.account_id,
qry_person_id: $slct.person_id,
log_lvl: 1
}),
qry__activity_log({
api_cfg: $ae_api,
account_id: $ae_loc.account_id,
qry_person_id: $slct.person_id,
limit: 10,
log_lvl: 1
})
]);
related_events = events || [];
related_posts = posts || [];
related_activity_logs = logs || [];
loading_activity = false;
}
@@ -294,7 +304,7 @@
<span>Linked Activity & Content</span>
</header>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- Related Events -->
<div class="space-y-3">
<h4 class="h4 flex items-center gap-2 text-sm opacity-70 uppercase tracking-wider font-bold">
@@ -308,8 +318,8 @@
<div class="space-y-2">
{#each related_events as ev}
<a href="/events/{ev.event_id_random}" class="card p-3 variant-soft flex flex-col gap-1 hover:variant-soft-primary transition-all">
<span class="font-bold">{ev.name}</span>
<span class="text-xs opacity-60">{new Date(ev.start_datetime).toLocaleDateString()}</span>
<span class="font-bold text-sm">{ev.name}</span>
<span class="text-[10px] opacity-60">{new Date(ev.start_datetime).toLocaleDateString()}</span>
</a>
{/each}
</div>
@@ -328,17 +338,46 @@
{:else}
<div class="space-y-2">
{#each related_posts as post}
<a href="/posts/{post.post_id_random}" class="card p-3 variant-soft flex flex-col gap-1 hover:variant-soft-primary transition-all">
<span class="font-bold">{post.title}</span>
<div class="flex justify-between items-center text-xs opacity-60">
<a href="/idaa/bb/{post.post_id_random}" class="card p-3 variant-soft flex flex-col gap-1 hover:variant-soft-primary transition-all">
<span class="font-bold text-sm">{post.title}</span>
<div class="flex justify-between items-center text-[10px] opacity-60">
<span>{new Date(post.created_on).toLocaleDateString()}</span>
<span class="badge variant-soft-surface">{post.post_comment_count || 0} comments</span>
<span class="badge variant-soft-surface scale-75">{post.post_comment_count || 0} comments</span>
</div>
</a>
{/each}
</div>
{/if}
</div>
<!-- Recent Activity -->
<div class="space-y-3">
<h4 class="h4 flex items-center gap-2 text-sm opacity-70 uppercase tracking-wider font-bold">
<Activity size={16} /> Recent Activity
</h4>
{#if loading_activity}
<div class="placeholder animate-pulse h-20 w-full"></div>
{:else if related_activity_logs.length === 0}
<p class="text-sm italic opacity-50">No recent activity logs.</p>
{:else}
<div class="space-y-2">
{#each related_activity_logs as log}
<div class="card p-3 variant-soft flex flex-col gap-1">
<div class="flex justify-between items-start gap-2">
<span class="badge variant-filled-surface text-[9px] uppercase tracking-tighter">{log.action}</span>
<span class="text-[9px] opacity-50">{new Date(log.created_on).toLocaleDateString()}</span>
</div>
{#if log.summary}
<span class="text-xs opacity-80 line-clamp-1">{log.summary}</span>
{/if}
</div>
{/each}
<a href="/core/activity_logs?person_id={$slct.person_id}" class="btn btn-sm variant-soft-surface w-full text-[10px]">
View All Activity
</a>
</div>
{/if}
</div>
</div>
</div>
{/if}