rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled

rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61)

Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
This commit was merged in pull request #61.
This commit is contained in:
2026-05-21 16:28:40 +02:00
committed by Hartmut
parent d9a7ec0338
commit b41c1d2501
943 changed files with 24548 additions and 16832 deletions
+89 -37
View File
@@ -1,5 +1,5 @@
import { Redis } from "ioredis";
import { PermissionKey, SSE_EVENT_TYPES, type SseEventType } from "@capakraken/shared";
import { PermissionKey, SSE_EVENT_TYPES, type SseEventType } from "@nexus/shared";
import { logger } from "../lib/logger.js";
export type UserSseAudience = `user:${string}`;
@@ -73,7 +73,11 @@ function assertValidSseAudience(audience: string): SseAudience {
}
export function canonicalizeSseAudiences(audiences?: Iterable<SseAudience>): SseAudience[] {
return [...new Set(Array.from(audiences ?? [], (audience) => assertValidSseAudience(audience)).filter(Boolean))].sort();
return [
...new Set(
Array.from(audiences ?? [], (audience) => assertValidSseAudience(audience)).filter(Boolean),
),
].sort();
}
function getBufferKey(type: SseEventType, audience: readonly SseAudience[]): string {
@@ -139,7 +143,12 @@ function flushEventType(type: SseEventType, audience: readonly SseAudience[]): v
const event: SseEvent =
entry.payloads.length === 1
? { type, payload: entry.payloads[0]!, timestamp: entry.firstTimestamp, audience: entry.audience }
? {
type,
payload: entry.payloads[0]!,
timestamp: entry.firstTimestamp,
audience: entry.audience,
}
: {
type,
payload: { _batch: entry.payloads },
@@ -159,7 +168,12 @@ export function flushPendingEvents(): void {
const [type] = key.split("::") as [SseEventType];
const event: SseEvent =
entry.payloads.length === 1
? { type, payload: entry.payloads[0]!, timestamp: entry.firstTimestamp, audience: entry.audience }
? {
type,
payload: entry.payloads[0]!,
timestamp: entry.firstTimestamp,
audience: entry.audience,
}
: {
type,
payload: { _batch: entry.payloads },
@@ -180,11 +194,13 @@ export function cancelPendingEvents(): void {
}
// Redis connection — use env var REDIS_URL or fallback to default dev URL
const REDIS_URL = process.env["REDIS_URL"] ?? (
process.env["NODE_ENV"] !== "production"
const REDIS_URL =
process.env["REDIS_URL"] ??
(process.env["NODE_ENV"] !== "production"
? (console.warn("[env] REDIS_URL not set, using localhost fallback"), "redis://localhost:6380")
: (() => { throw new Error("REDIS_URL required in production"); })()
);
: (() => {
throw new Error("REDIS_URL required in production");
})());
const CHANNEL = "capakraken:sse";
let publisher: Redis | null = null;
@@ -194,7 +210,10 @@ function getPublisher(): Redis {
if (!publisher) {
publisher = new Redis(REDIS_URL, { lazyConnect: false, enableReadyCheck: false });
publisher.on("error", (e: unknown) => {
logger.warn({ err: e, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis publisher emitted an error");
logger.warn(
{ err: e, redisUrl: REDIS_URL, channel: CHANNEL },
"Redis publisher emitted an error",
);
});
}
return publisher;
@@ -205,10 +224,16 @@ function setupSubscriber(): void {
try {
subscriber = new Redis(REDIS_URL, { lazyConnect: false, enableReadyCheck: false });
subscriber.on("error", (e: unknown) => {
logger.warn({ err: e, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis subscriber emitted an error");
logger.warn(
{ err: e, redisUrl: REDIS_URL, channel: CHANNEL },
"Redis subscriber emitted an error",
);
});
void subscriber.subscribe(CHANNEL).catch((err: unknown) => {
logger.warn({ err, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis subscribe failed for SSE event bus");
logger.warn(
{ err, redisUrl: REDIS_URL, channel: CHANNEL },
"Redis subscribe failed for SSE event bus",
);
});
subscriber.on("message", (_channel: string, message: string) => {
try {
@@ -229,7 +254,10 @@ function setupSubscriber(): void {
}
});
} catch (e) {
logger.warn({ err: e, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis unavailable, SSE event bus will run in local-only mode");
logger.warn(
{ err: e, redisUrl: REDIS_URL, channel: CHANNEL },
"Redis unavailable, SSE event bus will run in local-only mode",
);
}
}
@@ -257,26 +285,38 @@ class EventBus {
// Broadcast via Redis (all instances receive via subscriber.on("message"))
try {
const pub = getPublisher();
void pub.publish(
CHANNEL,
JSON.stringify({
type: normalizedEvent.type,
payload: normalizedEvent.payload,
timestamp: normalizedEvent.timestamp,
audience: normalizedEvent.audience,
}),
).catch((e: unknown) => {
logger.warn({ err: e, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis publish promise rejected, falling back to local-only SSE delivery");
publishLocal(normalizedEvent);
});
void pub
.publish(
CHANNEL,
JSON.stringify({
type: normalizedEvent.type,
payload: normalizedEvent.payload,
timestamp: normalizedEvent.timestamp,
audience: normalizedEvent.audience,
}),
)
.catch((e: unknown) => {
logger.warn(
{ err: e, redisUrl: REDIS_URL, channel: CHANNEL },
"Redis publish promise rejected, falling back to local-only SSE delivery",
);
publishLocal(normalizedEvent);
});
} catch (e) {
logger.warn({ err: e, redisUrl: REDIS_URL, channel: CHANNEL }, "Redis publish failed, falling back to local-only SSE delivery");
logger.warn(
{ err: e, redisUrl: REDIS_URL, channel: CHANNEL },
"Redis publish failed, falling back to local-only SSE delivery",
);
// Deliver locally when Redis is unavailable
publishLocal(normalizedEvent);
}
}
emit(type: SseEventType, payload: Record<string, unknown>, audience: Iterable<SseAudience> = []): void {
emit(
type: SseEventType,
payload: Record<string, unknown>,
audience: Iterable<SseAudience> = [],
): void {
this.publish({
type,
payload,
@@ -327,7 +367,11 @@ export const emitAllocationCreated = (allocation: Record<string, unknown>) =>
export const emitAllocationUpdated = (allocation: Record<string, unknown>) =>
eventBus.emit(SSE_EVENT_TYPES.ALLOCATION_UPDATED, allocation, buildPlanningAudiences(allocation));
export const emitAllocationDeleted = (allocationId: string, projectId: string, resourceId?: string | null) =>
export const emitAllocationDeleted = (
allocationId: string,
projectId: string,
resourceId?: string | null,
) =>
eventBus.emit(
SSE_EVENT_TYPES.ALLOCATION_DELETED,
{ allocationId, projectId, ...(resourceId ? { resourceId } : {}) },
@@ -338,11 +382,9 @@ export const emitProjectShifted = (project: Record<string, unknown>) =>
eventBus.emit(SSE_EVENT_TYPES.PROJECT_SHIFTED, project, buildPlanningAudiences(project));
export const emitBudgetWarning = (projectId: string, payload: Record<string, unknown>) =>
eventBus.emit(
SSE_EVENT_TYPES.BUDGET_WARNING,
{ projectId, ...payload },
[permissionAudience(PermissionKey.MANAGE_ALLOCATIONS)],
);
eventBus.emit(SSE_EVENT_TYPES.BUDGET_WARNING, { projectId, ...payload }, [
permissionAudience(PermissionKey.MANAGE_ALLOCATIONS),
]);
export const emitVacationCreated = (vacation: Record<string, unknown>) =>
eventBus.emit(SSE_EVENT_TYPES.VACATION_CREATED, vacation, buildPlanningAudiences(vacation));
@@ -358,16 +400,24 @@ export const emitVacationDeleted = (vacationId: string, resourceId: string) =>
);
export const emitRoleCreated = (role: Record<string, unknown>) =>
eventBus.emit(SSE_EVENT_TYPES.ROLE_CREATED, role, [permissionAudience(PermissionKey.MANAGE_ROLES)]);
eventBus.emit(SSE_EVENT_TYPES.ROLE_CREATED, role, [
permissionAudience(PermissionKey.MANAGE_ROLES),
]);
export const emitRoleUpdated = (role: Record<string, unknown>) =>
eventBus.emit(SSE_EVENT_TYPES.ROLE_UPDATED, role, [permissionAudience(PermissionKey.MANAGE_ROLES)]);
eventBus.emit(SSE_EVENT_TYPES.ROLE_UPDATED, role, [
permissionAudience(PermissionKey.MANAGE_ROLES),
]);
export const emitRoleDeleted = (roleId: string) =>
eventBus.emit(SSE_EVENT_TYPES.ROLE_DELETED, { roleId }, [permissionAudience(PermissionKey.MANAGE_ROLES)]);
eventBus.emit(SSE_EVENT_TYPES.ROLE_DELETED, { roleId }, [
permissionAudience(PermissionKey.MANAGE_ROLES),
]);
export function emitNotificationCreated(userId: string, notificationId: string): void {
eventBus.emit(SSE_EVENT_TYPES.NOTIFICATION_CREATED, { userId, notificationId }, [userAudience(userId)]);
eventBus.emit(SSE_EVENT_TYPES.NOTIFICATION_CREATED, { userId, notificationId }, [
userAudience(userId),
]);
}
export function emitTaskAssigned(userId: string, notificationId: string): void {
@@ -379,7 +429,9 @@ export function emitTaskCompleted(userId: string, notificationId: string): void
}
export function emitTaskStatusChanged(userId: string, notificationId: string): void {
eventBus.emit(SSE_EVENT_TYPES.TASK_STATUS_CHANGED, { userId, notificationId }, [userAudience(userId)]);
eventBus.emit(SSE_EVENT_TYPES.TASK_STATUS_CHANGED, { userId, notificationId }, [
userAudience(userId),
]);
}
export function emitReminderDue(userId: string, notificationId: string): void {
+6 -1
View File
@@ -1,4 +1,9 @@
import { resolvePermissions, type PermissionKey, type PermissionOverrides, SystemRole } from "@capakraken/shared";
import {
resolvePermissions,
type PermissionKey,
type PermissionOverrides,
SystemRole,
} from "@nexus/shared";
import {
canonicalizeSseAudiences,
permissionAudience,