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:
@@ -181,52 +181,112 @@ export async function update_ae_obj__activity_log({
|
|||||||
return ae_promises.update__activity_log_obj;
|
return ae_promises.update__activity_log_obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated 2026-01-06
|
// Updated 2026-01-07
|
||||||
|
|
||||||
export async function qry__activity_log({
|
export async function qry__activity_log({
|
||||||
|
|
||||||
api_cfg,
|
api_cfg,
|
||||||
|
|
||||||
account_id,
|
account_id,
|
||||||
|
|
||||||
qry_str,
|
qry_str,
|
||||||
|
|
||||||
|
qry_person_id = null,
|
||||||
|
|
||||||
enabled = 'enabled',
|
enabled = 'enabled',
|
||||||
|
|
||||||
hidden = 'not_hidden',
|
hidden = 'not_hidden',
|
||||||
|
|
||||||
view = 'default',
|
view = 'default',
|
||||||
|
|
||||||
limit = 50,
|
limit = 50,
|
||||||
|
|
||||||
offset = 0,
|
offset = 0,
|
||||||
|
|
||||||
order_by_li = { created_on: 'DESC' },
|
order_by_li = { created_on: 'DESC' },
|
||||||
|
|
||||||
log_lvl = 0
|
log_lvl = 0
|
||||||
|
|
||||||
}: {
|
}: {
|
||||||
|
|
||||||
api_cfg: any;
|
api_cfg: any;
|
||||||
|
|
||||||
account_id: string;
|
account_id: string;
|
||||||
|
|
||||||
qry_str?: string;
|
qry_str?: string;
|
||||||
|
|
||||||
|
qry_person_id?: string | null;
|
||||||
|
|
||||||
enabled?: 'enabled' | 'all' | 'not_enabled';
|
enabled?: 'enabled' | 'all' | 'not_enabled';
|
||||||
|
|
||||||
hidden?: 'hidden' | 'all' | 'not_hidden';
|
hidden?: 'hidden' | 'all' | 'not_hidden';
|
||||||
|
|
||||||
view?: string;
|
view?: string;
|
||||||
|
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
|
||||||
offset?: number;
|
offset?: number;
|
||||||
|
|
||||||
order_by_li?: Record<string, 'ASC' | 'DESC'>;
|
order_by_li?: Record<string, 'ASC' | 'DESC'>;
|
||||||
|
|
||||||
log_lvl?: number;
|
log_lvl?: number;
|
||||||
|
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const search_query: any = { and: [] };
|
const search_query: any = { and: [] };
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (account_id) {
|
if (account_id) {
|
||||||
|
|
||||||
search_query.and.push({ field: 'account_id_random', op: 'eq', value: account_id });
|
search_query.and.push({ field: 'account_id_random', op: 'eq', value: account_id });
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (qry_str) {
|
if (qry_str) {
|
||||||
|
|
||||||
search_query.q = 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({
|
ae_promises.load__activity_log_obj_li = await api.search_ae_obj_v3({
|
||||||
|
|
||||||
api_cfg,
|
api_cfg,
|
||||||
|
|
||||||
obj_type: 'activity_log',
|
obj_type: 'activity_log',
|
||||||
|
|
||||||
search_query,
|
search_query,
|
||||||
|
|
||||||
enabled,
|
enabled,
|
||||||
|
|
||||||
hidden,
|
hidden,
|
||||||
|
|
||||||
view,
|
view,
|
||||||
|
|
||||||
limit,
|
limit,
|
||||||
|
|
||||||
offset,
|
offset,
|
||||||
|
|
||||||
order_by_li,
|
order_by_li,
|
||||||
|
|
||||||
log_lvl
|
log_lvl
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return ae_promises.load__activity_log_obj_li;
|
return ae_promises.load__activity_log_obj_li;
|
||||||
}
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<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';
|
import { ae_loc, slct } from '$lib/stores/ae_stores';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -70,6 +70,18 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</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 -->
|
<!-- Lookups Card -->
|
||||||
<div class="card p-4 space-y-4 variant-soft-surface">
|
<div class="card p-4 space-y-4 variant-soft-surface">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|||||||
179
src/routes/core/activity_logs/+page.svelte
Normal file
179
src/routes/core/activity_logs/+page.svelte
Normal 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>
|
||||||
5
src/routes/core/activity_logs/+page.ts
Normal file
5
src/routes/core/activity_logs/+page.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/** @type {import('./$types').PageLoad} */
|
||||||
|
export async function load({ parent }) {
|
||||||
|
const data = await parent();
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -31,7 +31,8 @@
|
|||||||
import { update_ae_obj__person } from '$lib/ae_core/ae_core__person';
|
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_ae_obj_li__event } from '$lib/ae_events/ae_events__event';
|
||||||
import { qry__post } from '$lib/ae_posts/ae_posts__post';
|
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 {
|
interface Props {
|
||||||
data: any;
|
data: any;
|
||||||
@@ -59,6 +60,7 @@
|
|||||||
|
|
||||||
let related_events: any[] = $state([]);
|
let related_events: any[] = $state([]);
|
||||||
let related_posts: any[] = $state([]);
|
let related_posts: any[] = $state([]);
|
||||||
|
let related_activity_logs: any[] = $state([]);
|
||||||
let loading_activity = $state(false);
|
let loading_activity = $state(false);
|
||||||
|
|
||||||
async function load_activity() {
|
async function load_activity() {
|
||||||
@@ -66,7 +68,7 @@
|
|||||||
loading_activity = true;
|
loading_activity = true;
|
||||||
|
|
||||||
// Load related data using search queries
|
// Load related data using search queries
|
||||||
const [events, posts] = await Promise.all([
|
const [events, posts, logs] = await Promise.all([
|
||||||
qry_ae_obj_li__event({
|
qry_ae_obj_li__event({
|
||||||
api_cfg: $ae_api,
|
api_cfg: $ae_api,
|
||||||
for_obj_id: $ae_loc.account_id,
|
for_obj_id: $ae_loc.account_id,
|
||||||
@@ -78,11 +80,19 @@
|
|||||||
account_id: $ae_loc.account_id,
|
account_id: $ae_loc.account_id,
|
||||||
qry_person_id: $slct.person_id,
|
qry_person_id: $slct.person_id,
|
||||||
log_lvl: 1
|
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_events = events || [];
|
||||||
related_posts = posts || [];
|
related_posts = posts || [];
|
||||||
|
related_activity_logs = logs || [];
|
||||||
loading_activity = false;
|
loading_activity = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,7 +304,7 @@
|
|||||||
<span>Linked Activity & Content</span>
|
<span>Linked Activity & Content</span>
|
||||||
</header>
|
</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 -->
|
<!-- Related Events -->
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<h4 class="h4 flex items-center gap-2 text-sm opacity-70 uppercase tracking-wider font-bold">
|
<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">
|
<div class="space-y-2">
|
||||||
{#each related_events as ev}
|
{#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">
|
<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="font-bold text-sm">{ev.name}</span>
|
||||||
<span class="text-xs opacity-60">{new Date(ev.start_datetime).toLocaleDateString()}</span>
|
<span class="text-[10px] opacity-60">{new Date(ev.start_datetime).toLocaleDateString()}</span>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -328,17 +338,46 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each related_posts as post}
|
{#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">
|
<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">{post.title}</span>
|
<span class="font-bold text-sm">{post.title}</span>
|
||||||
<div class="flex justify-between items-center text-xs opacity-60">
|
<div class="flex justify-between items-center text-[10px] opacity-60">
|
||||||
<span>{new Date(post.created_on).toLocaleDateString()}</span>
|
<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>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user