Skip to content

Commit 91495f4

Browse files
committed
Opey working, site map public, login redirect_to , activity trail WIP
1 parent 28404df commit 91495f4

21 files changed

Lines changed: 417 additions & 38 deletions

File tree

src/hooks.server.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import { sveltekitSessionHandle } from "svelte-kit-sessions";
77
import RedisStore from "svelte-kit-connect-redis";
88
import { Redis } from "ioredis";
99
import { env } from "$env/dynamic/private";
10+
import { PUBLIC_OBP_BASE_URL } from "$env/static/public";
1011
import { oauth2ProviderManager } from "$lib/oauth/providerManager";
1112
import { SessionOAuthHelper } from "$lib/oauth/sessionHelper";
1213
import { resourceDocsCache } from "$lib/stores/resourceDocsCache";
14+
import { healthCheckRegistry } from "$lib/health-check/HealthCheckRegistry";
15+
import { ensureSystemActivityTrail } from "$lib/opey/bootstrap/activityTrailEntities";
1316

1417
declare const process: { env: Record<string, string | undefined>; argv: string[] };
1518

@@ -83,6 +86,16 @@ if (!env.REDIS_HOST || !env.REDIS_PORT) {
8386
// Start OAuth2 provider manager (handles initialization and retries automatically)
8487
await oauth2ProviderManager.start();
8588

89+
// Register and start health checks
90+
healthCheckRegistry.register({ serviceName: 'OBP API', url: `${PUBLIC_OBP_BASE_URL}/obp/v6.0.0/root` });
91+
if (env.OPEY_BASE_URL) {
92+
healthCheckRegistry.register({ serviceName: 'Opey II', url: `${env.OPEY_BASE_URL}/status` });
93+
}
94+
healthCheckRegistry.startAll();
95+
96+
// Bootstrap: ensure activity trail dynamic entity exists (attempted once per server lifecycle)
97+
let activityTrailBootstrapped = false;
98+
8699
function needsAuthorization(routeId: string): boolean {
87100
// protected routes are put in the /(protected)/ route group
88101
return routeId.startsWith("/(protected)/");
@@ -106,10 +119,11 @@ const checkAuthorization: Handle = async ({ event, resolve }) => {
106119
"No valid OAuth data found in session. Redirecting to login.",
107120
);
108121
// Redirect to login page if no OAuth data is found
122+
const redirectTo = encodeURIComponent(event.url.pathname + event.url.search);
109123
return new Response(null, {
110124
status: 302,
111125
headers: {
112-
Location: "/login",
126+
Location: `/login?redirect_to=${redirectTo}`,
113127
},
114128
});
115129
}
@@ -134,21 +148,23 @@ const checkAuthorization: Handle = async ({ event, resolve }) => {
134148
logger.info("Destroying expired session and redirecting to login.");
135149
await session.destroy();
136150

151+
const redirectTo = encodeURIComponent(event.url.pathname + event.url.search);
137152
return new Response(null, {
138153
status: 302,
139154
headers: {
140-
Location: "/login",
155+
Location: `/login?redirect_to=${redirectTo}`,
141156
},
142157
});
143158
}
144159
}
145160

146161
if (!session || !session.data.user) {
147162
// Redirect to login page if not authenticated
163+
const redirectTo = encodeURIComponent(event.url.pathname + event.url.search);
148164
return new Response(null, {
149165
status: 302,
150166
headers: {
151-
Location: "/login",
167+
Location: `/login?redirect_to=${redirectTo}`,
152168
},
153169
});
154170
} else {
@@ -160,6 +176,21 @@ const checkAuthorization: Handle = async ({ event, resolve }) => {
160176
resourceDocsCache.preWarmCache(sessionOAuth.accessToken).catch(() => {
161177
// Silently fail - pre-warming is best-effort
162178
});
179+
180+
// Ensure system_activity_trail entity exists (once per server lifecycle)
181+
if (!activityTrailBootstrapped) {
182+
activityTrailBootstrapped = true;
183+
ensureSystemActivityTrail(sessionOAuth.accessToken).then((ok) => {
184+
if (!ok) {
185+
logger.warn(
186+
"WARNING: system_activity_trail entity could not be created. " +
187+
"Ensure the API Manager consumer has the CanCreateSystemLevelDynamicEntity scope. " +
188+
"Opey activity trail features will not work without it."
189+
);
190+
activityTrailBootstrapped = false; // Allow retry on next request
191+
}
192+
});
193+
}
163194
}
164195
}
165196
}

src/lib/components/ChatMessage.svelte

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@
7979
8080
// Compute alignment class
8181
let alignmentClass = $derived(message.role === 'user' ? 'items-end' : 'items-start');
82+
83+
// Use $effect.pre to ensure renderedContent updates before DOM render
84+
let renderedContent = $state('');
85+
$effect.pre(() => {
86+
renderedContent = renderMarkdown(message.message);
87+
});
8288
</script>
8389

8490
<!-- Message container -->
@@ -103,7 +109,7 @@
103109

104110
<!-- Message content -->
105111
<div
106-
class="{message.role === 'user' ? 'max-w-3/5' : message.role === 'tool' ? 'w-2/3' : 'max-w-full'} group relative mt-3"
112+
class="{message.role === 'user' ? 'max-w-3/5' : message.role === 'tool' ? 'w-2/3' : 'w-full'} group relative mt-3"
107113
role="region"
108114
aria-label="Chat message"
109115
>
@@ -165,7 +171,7 @@
165171
{:else}
166172
<hr class="hr" />
167173
<div class="prose max-w-full rounded-2xl p-2 text-left dark:prose-invert">
168-
{@html renderMarkdown(message.message)}
174+
{@html renderedContent}
169175
{#if message.error}
170176
<div class="mt-2">
171177
<p class="text-sm text-error-500 dark:text-error-400">

src/lib/components/OpeyChat.svelte

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import { ChatController } from '$lib/opey/controllers/ChatController';
1414
import { SessionState, type SessionSnapshot } from '$lib/opey/state/SessionState';
1515
import { ConsentSessionService } from '$lib/opey/services/ConsentSessionService';
16-
import type { ToolMessage } from '$lib/opey/types';
16+
import type { BaseMessage, ToolMessage } from '$lib/opey/types';
1717
import type { OBPConsentInfo } from '$lib/obp/types';
1818
import { healthCheckRegistry } from '$lib/health-check/HealthCheckRegistry';
1919
@@ -52,7 +52,7 @@
5252
}
5353
// Default chat options
5454
const defaultChatOptions: OpeyChatOptions = {
55-
baseUrl: env.PUBLIC_OPEY_BASE_URL || 'http://localhost:5000',
55+
baseUrl: '/api/opey',
5656
displayHeader: true,
5757
currentlyActiveUserName: 'Guest',
5858
displayConnectionPips: true,
@@ -74,6 +74,7 @@
7474
7575
let session: SessionSnapshot = $state({ isAuthenticated: userAuthenticated, status: 'ready' });
7676
let chat: ChatStateSnapshot = $state({ threadId: '', messages: [] });
77+
let messages: BaseMessage[] = $state([]);
7778
7879
// Track pending approvals for batch handling
7980
let pendingApprovalTools = $derived.by(() => {
@@ -215,6 +216,7 @@
215216
sessionState.subscribe((s) => (session = s));
216217
chatState.subscribe((c) => {
217218
chat = c;
219+
messages = [...c.messages];
218220
});
219221
220222
if (options.initialAssistantMessage) {
@@ -558,10 +560,10 @@
558560
class="h-full w-full overflow-y-auto overflow-x-hidden py-4 {options.bodyClasses || ''}"
559561
>
560562
<div class="space-y-4 min-w-0">
561-
{#each chat.messages as message, index (message.id)}
563+
{#each messages as message, index (message.id)}
562564
<ChatMessage
563565
{message}
564-
previousMessageRole={index > 0 ? chat.messages[index - 1].role : undefined}
566+
previousMessageRole={index > 0 ? messages[index - 1].role : undefined}
565567
userName={options.currentlyActiveUserName}
566568
onApprove={handleApprove}
567569
onDeny={handleDeny}
@@ -570,7 +572,7 @@
570572
batchApprovalGroup={pendingApprovalTools.length > 1 ? pendingApprovalTools : undefined}
571573
onConsent={handleConsent}
572574
onConsentDeny={handleConsentDeny}
573-
allMessages={chat.messages}
575+
allMessages={messages}
574576
/>
575577
{/each}
576578
</div>

src/lib/config/navigation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function buildMyAccountItems(): NavigationItem[] {
7272
label: "My Collections",
7373
iconComponent: FolderOpen,
7474
},
75-
{ href: "/user/site-map", label: "Site Map", iconComponent: Map },
75+
{ href: "/site-map", label: "Site Map", iconComponent: Map },
7676
];
7777

7878
// Only add Subscriptions link if PUBLIC_SUBSCRIPTIONS_URL is set
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { obp_requests } from "$lib/obp/requests";
2+
import { createLogger } from "$lib/utils/logger";
3+
4+
const logger = createLogger("ActivityTrailBootstrap");
5+
6+
/**
7+
* Schema definition for system_activity_trail — a system-level personal dynamic entity.
8+
*
9+
* Stores snapshots of data entities the user has encountered (users, consumers,
10+
* customers, accounts, products, etc.) to power Opey's "What should I know?" insights.
11+
*
12+
* Bank-scoped entities include a bank_id in the metadata field.
13+
*/
14+
const SYSTEM_ACTIVITY_TRAIL = {
15+
entity_name: "system_activity_trail",
16+
has_personal_entity: true,
17+
has_public_access: false,
18+
has_community_access: false,
19+
personal_requires_role: false,
20+
schema: {
21+
description:
22+
"Snapshots of data entities encountered by the user, for cross-entity insight generation.",
23+
required: ["timestamp", "entity_type", "entity_id", "summary"],
24+
properties: {
25+
timestamp: {
26+
type: "string",
27+
minLength: 20,
28+
maxLength: 30,
29+
example: "2026-03-06T14:32:00Z",
30+
description: "ISO 8601 timestamp of when the entity was encountered"
31+
},
32+
entity_type: {
33+
type: "string",
34+
minLength: 1,
35+
maxLength: 50,
36+
example: "customer",
37+
description:
38+
"Type of entity encountered, e.g. user, consumer, entitlement, customer, account, product, transaction, branch, atm"
39+
},
40+
entity_id: {
41+
type: "string",
42+
minLength: 1,
43+
maxLength: 200,
44+
example: "abc-123",
45+
description: "The specific OBP identifier of the entity"
46+
},
47+
summary: {
48+
type: "string",
49+
minLength: 1,
50+
maxLength: 2000,
51+
example:
52+
"ABC Corp - 3 accounts (2 GBP, 1 EUR), onboarded 2024-01, KYC complete",
53+
description:
54+
"Human-readable snapshot of the entity data at time of encounter"
55+
},
56+
metadata: {
57+
type: "string",
58+
minLength: 0,
59+
maxLength: 5000,
60+
example:
61+
'{"bank_id": "gh.29.uk", "account_count": 3, "currencies": ["GBP", "EUR"]}',
62+
description:
63+
"JSON-encoded structured data for machine reasoning. Include bank_id here for bank-scoped entities."
64+
}
65+
}
66+
}
67+
};
68+
69+
/**
70+
* Ensure the system_activity_trail dynamic entity exists in OBP.
71+
* Creates it if missing. Requires CanCreateSystemLevelDynamicEntity consumer scope.
72+
*
73+
* Call once at startup (on first authenticated request).
74+
*/
75+
export async function ensureSystemActivityTrail(
76+
accessToken: string
77+
): Promise<boolean> {
78+
const entityName = SYSTEM_ACTIVITY_TRAIL.entity_name;
79+
80+
try {
81+
const response = await obp_requests.get(
82+
"/obp/v6.0.0/management/system-dynamic-entities",
83+
accessToken
84+
);
85+
const entities = response.dynamic_entities || [];
86+
const exists = entities.some(
87+
(e: { entity_name: string }) => e.entity_name === entityName
88+
);
89+
90+
if (exists) {
91+
logger.info(`Dynamic entity '${entityName}' already exists.`);
92+
return true;
93+
}
94+
} catch (err) {
95+
logger.warn(
96+
`Could not list system dynamic entities — ` +
97+
`check that the API Manager consumer has CanCreateSystemLevelDynamicEntity scope: ${err}`
98+
);
99+
return false;
100+
}
101+
102+
try {
103+
logger.info(`Creating dynamic entity '${entityName}'...`);
104+
await obp_requests.post(
105+
"/obp/v6.0.0/management/system-dynamic-entities",
106+
SYSTEM_ACTIVITY_TRAIL,
107+
accessToken
108+
);
109+
logger.info(`Dynamic entity '${entityName}' created successfully.`);
110+
return true;
111+
} catch (err) {
112+
logger.warn(
113+
`Failed to create dynamic entity '${entityName}'. ` +
114+
`Ensure the API Manager consumer has CanCreateSystemLevelDynamicEntity scope. Error: ${err}`
115+
);
116+
return false;
117+
}
118+
}

src/lib/opey/services/OBPIntegrationService.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,25 @@ export class DefaultOBPIntegrationService implements OBPIntegrationService {
1919
throw new Error("User not authenticated with OBP");
2020
}
2121

22+
logger.info(`getOrCreateOpeyConsent: looking for existing ACCEPTED consent with consumer_id="${this.opeyConsumerId}"`);
23+
2224
// Check for existing consent first
2325
const existingConsentId = await this.checkExistingOpeyConsent(session);
2426
if (existingConsentId) {
2527
const userIdentifier = extractUsernameFromJWT(existingConsentId.jwt);
2628
logger.info(
27-
`Found existing consent JWT - Primary user: ${userIdentifier}`,
29+
`Found existing consent: consent_id="${existingConsentId.consent_id}", status="${existingConsentId.status}", consumer_id="${existingConsentId.consumer_id}" - user: ${userIdentifier}`,
2830
);
2931
return existingConsentId;
3032
}
3133

3234
// Create new consent
35+
logger.info(`No existing ACCEPTED consent found - creating new IMPLICIT consent with consumer_id="${this.opeyConsumerId}"`);
3336
const consent = await this.createImplicitConsent(
3437
session.data.oauth.access_token,
3538
);
3639
const userIdentifier = extractUsernameFromJWT(consent.jwt);
37-
logger.info(`Created new consent JWT - Primary user: ${userIdentifier}`);
40+
logger.info(`Created new consent: consent_id="${consent.consent_id}", status="${consent.status}", consumer_id="${consent.consumer_id}" - user: ${userIdentifier}`);
3841
return consent;
3942
}
4043

src/lib/opey/services/RestChatService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,6 @@ export class RestChatService implements ChatService {
207207
buffer = lines.pop() || ''; // Keep the last incomplete line
208208

209209
for (const line of lines) {
210-
//logger.debug(line);
211210
if (line.startsWith('data: ')) {
212211
let eventData;
213212

@@ -233,7 +232,7 @@ export class RestChatService implements ChatService {
233232
}
234233

235234
private handleStreamEvent(eventData: any): void {
236-
logger.debug('Received stream event data:', eventData);
235+
logger.debug(`Received stream event type="${eventData?.type}":`, eventData);
237236
switch (eventData.type) {
238237
case 'user_message_confirmed':
239238
this.streamEventCallback?.({
@@ -248,7 +247,8 @@ export class RestChatService implements ChatService {
248247
this.streamEventCallback?.({
249248
type: 'assistant_start',
250249
messageId: eventData.message_id,
251-
timestamp: new Date(eventData.timestamp)
250+
// Opey sends Unix timestamps in seconds; multiply by 1000 for JS Date (milliseconds)
251+
timestamp: new Date(eventData.timestamp * 1000)
252252
});
253253
break;
254254
case 'assistant_token':

0 commit comments

Comments
 (0)