mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-04-12 02:18:18 +00:00
feat: team module, multi-stage Dockerfile, doctor utils, AI config overhaul
- Team module: POST /api/team/members (in-place employee creation with temp password + Redis cache), PUT /api/team/members/:id, GET temp password endpoint. Uses signUpInWorkspace — no email invites. - Dockerfile: rewritten as multi-stage build (builder + runtime) so native modules compile for target arch. Fixes darwin→linux crash. - .dockerignore: exclude dist, node_modules, .env, .git, data/ - package-lock.json: regenerated against public npmjs.org (was pointing at localhost:4873 Verdaccio — broke docker builds) - Doctor utils: shared DOCTOR_VISIT_SLOTS_FRAGMENT + normalizeDoctors helper for visit-slot-aware queries across 6 consumers - AI config: full admin CRUD (GET/PUT/POST reset), workspace-scoped setup-state with workspace ID isolation, AI prompt defaults overhaul - Agent config: camelCase field fix for SDK-synced workspaces - Session service: workspace-scoped Redis key prefixing for setup state - Recordings/supervisor/widget services: updated to use doctor-utils shared fragments instead of inline visitingHours queries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { PlatformGraphqlService } from '../platform/platform-graphql.service';
|
||||
import {
|
||||
DEFAULT_SETUP_STATE,
|
||||
SETUP_STEP_NAMES,
|
||||
@@ -14,13 +15,34 @@ const SETUP_STATE_PATH = join(process.cwd(), 'data', 'setup-state.json');
|
||||
// pattern of ThemeService and WidgetConfigService — load on init, cache in
|
||||
// memory, write on every change. No backups (the data is small and easily
|
||||
// recreated by the wizard if it ever gets corrupted).
|
||||
//
|
||||
// Workspace scoping: the sidecar's API key is scoped to exactly one
|
||||
// workspace, so on first access we compare the file's stored workspaceId
|
||||
// against the live currentWorkspace.id from the platform. If they differ
|
||||
// (DB reset, re-onboard, sidecar pointed at a new workspace), the file is
|
||||
// reset before any reads return. This guarantees a fresh wizard for a
|
||||
// fresh workspace without manual file deletion.
|
||||
@Injectable()
|
||||
export class SetupStateService implements OnModuleInit {
|
||||
private readonly logger = new Logger(SetupStateService.name);
|
||||
private cached: SetupState | null = null;
|
||||
// Memoize the platform's currentWorkspace.id lookup so we don't hit
|
||||
// the platform on every getState() call. Set once per process boot
|
||||
// (or after a successful reset).
|
||||
private liveWorkspaceId: string | null = null;
|
||||
private workspaceCheckPromise: Promise<void> | null = null;
|
||||
|
||||
constructor(private platform: PlatformGraphqlService) {}
|
||||
|
||||
onModuleInit() {
|
||||
this.load();
|
||||
// Fire-and-forget the workspace probe so the file gets aligned
|
||||
// before the frontend's first GET. Errors are logged but
|
||||
// non-fatal — if the platform is down at boot, the legacy
|
||||
// unscoped behaviour kicks in until the first reachable probe.
|
||||
this.ensureWorkspaceMatch().catch((err) =>
|
||||
this.logger.warn(`Initial workspace probe failed: ${err}`),
|
||||
);
|
||||
}
|
||||
|
||||
getState(): SetupState {
|
||||
@@ -28,6 +50,59 @@ export class SetupStateService implements OnModuleInit {
|
||||
return this.load();
|
||||
}
|
||||
|
||||
// Awaits a workspace check before returning state. The controller
|
||||
// calls this so the GET response always reflects the current
|
||||
// workspace, not yesterday's.
|
||||
async getStateChecked(): Promise<SetupState> {
|
||||
await this.ensureWorkspaceMatch();
|
||||
return this.getState();
|
||||
}
|
||||
|
||||
private async ensureWorkspaceMatch(): Promise<void> {
|
||||
// Single-flight: if a check is already running, await it.
|
||||
if (this.workspaceCheckPromise) return this.workspaceCheckPromise;
|
||||
if (this.liveWorkspaceId) {
|
||||
// Already validated this process. Trust the cache.
|
||||
return;
|
||||
}
|
||||
this.workspaceCheckPromise = (async () => {
|
||||
try {
|
||||
const data = await this.platform.query<{
|
||||
currentWorkspace: { id: string };
|
||||
}>(`{ currentWorkspace { id } }`);
|
||||
const liveId = data?.currentWorkspace?.id ?? null;
|
||||
if (!liveId) {
|
||||
this.logger.warn(
|
||||
'currentWorkspace.id was empty — cannot scope setup-state',
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.liveWorkspaceId = liveId;
|
||||
const current = this.getState();
|
||||
if (current.workspaceId && current.workspaceId !== liveId) {
|
||||
this.logger.log(
|
||||
`Workspace changed (${current.workspaceId} → ${liveId}) — resetting setup-state`,
|
||||
);
|
||||
this.resetState();
|
||||
}
|
||||
if (!current.workspaceId) {
|
||||
// First boot after the workspaceId field was added
|
||||
// (or first boot ever). Stamp the file so future
|
||||
// boots can detect drift.
|
||||
const stamped: SetupState = {
|
||||
...this.getState(),
|
||||
workspaceId: liveId,
|
||||
};
|
||||
this.writeFile(stamped);
|
||||
this.cached = stamped;
|
||||
}
|
||||
} finally {
|
||||
this.workspaceCheckPromise = null;
|
||||
}
|
||||
})();
|
||||
return this.workspaceCheckPromise;
|
||||
}
|
||||
|
||||
// Returns true if any required step is incomplete and the wizard hasn't
|
||||
// been explicitly dismissed. Used by the frontend post-login redirect.
|
||||
isWizardRequired(): boolean {
|
||||
@@ -95,8 +170,16 @@ export class SetupStateService implements OnModuleInit {
|
||||
}
|
||||
|
||||
resetState(): SetupState {
|
||||
this.writeFile(DEFAULT_SETUP_STATE);
|
||||
this.cached = { ...DEFAULT_SETUP_STATE };
|
||||
// Preserve the live workspaceId on reset so the file remains
|
||||
// scoped — otherwise the next workspace check would think the
|
||||
// file is unscoped and re-stamp it, which is fine but creates
|
||||
// an extra write.
|
||||
const fresh: SetupState = {
|
||||
...DEFAULT_SETUP_STATE,
|
||||
workspaceId: this.liveWorkspaceId ?? null,
|
||||
};
|
||||
this.writeFile(fresh);
|
||||
this.cached = fresh;
|
||||
this.logger.log('Setup state reset to defaults');
|
||||
return this.cached;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user