thatcher 1.0.45 → 1.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/adapters/google-auth.js +0 -7
- package/src/adapters/google-drive.js +1 -3
- package/src/app/api/[entity]/[[...path]]/route.js +1 -1
- package/src/app/api/audit/logs/route.js +1 -1
- package/src/app/api/audit/permissions/route.js +0 -1
- package/src/app/api/audit/route.js +2 -4
- package/src/app/api/auth/google/callback/route.js +1 -1
- package/src/app/api/auth/google/route.js +1 -1
- package/src/app/api/auth/logout/route.js +1 -1
- package/src/app/api/csrf-token/route.js +1 -1
- package/src/app/api/domains/route.js +1 -1
- package/src/app/api/email/allocate/route.js +0 -1
- package/src/app/api/email/receive/route.js +1 -1
- package/src/app/api/health/route.js +1 -1
- package/src/cli.js +1 -1
- package/src/config/spec-helpers.js +0 -1
- package/src/engine.server.js +1 -1
- package/src/lib/alert-manager.js +1 -1
- package/src/lib/api-helpers.js +1 -1
- package/src/lib/auth-route-helpers.js +1 -1
- package/src/lib/business-rules-engine.js +3 -3
- package/src/lib/busybase/adapter.js +2 -2
- package/src/lib/busybase/store.js +1 -1
- package/src/lib/config-generator-engine.js +0 -2
- package/src/lib/crud-action-helpers.js +1 -1
- package/src/lib/crud-handlers.js +2 -2
- package/src/lib/csrf-protection.js +0 -1
- package/src/lib/debug-registry.js +1 -1
- package/src/lib/errors/wrap.js +1 -1
- package/src/lib/events-engine.js +1 -1
- package/src/lib/field-iterator.js +2 -2
- package/src/lib/next-shim.js +2 -2
- package/src/lib/perf.js +0 -2
- package/src/lib/request-trace.js +1 -4
- package/src/lib/stage-pipeline.js +0 -1
- package/src/lib/static-server.js +1 -1
- package/src/lib/tracing.js +0 -3
- package/src/lib/validation-middleware.js +1 -1
- package/src/lib/workflow-engine.js +1 -1
- package/src/lib/xstate-workflow-engine.js +1 -1
- package/src/server/server.js +5 -9
- package/src/services/collaborator-role.service.js +1 -3
- package/src/services/notification-engine.js +3 -3
- package/src/services/permission.service.js +1 -1
- package/src/ui/advanced-widgets.js +0 -3
- package/src/ui/auth-pages.js +1 -1
- package/src/ui/component-engine.js +2 -2
- package/src/ui/dashboard-renderer.js +1 -1
- package/src/ui/dialog-engine.js +7 -7
- package/src/ui/engagement-cards.js +0 -1
- package/src/ui/engagement-dialogs.js +0 -2
- package/src/ui/entity-renderer.js +2 -3
- package/src/ui/event-delegation.js +1 -1
- package/src/ui/file-dialogs.js +0 -2
- package/src/ui/job-management-renderer.js +0 -1
- package/src/ui/layout.js +3 -4
- package/src/ui/page-handler-admin.js +2 -2
- package/src/ui/page-handler-helpers.js +3 -3
- package/src/ui/page-handler-rfi.js +1 -1
- package/src/ui/page-handler.js +1 -1
- package/src/ui/permissions-ui.js +0 -12
- package/src/ui/picker-dialogs.js +0 -1
- package/src/ui/review/comparison.js +1 -1
- package/src/ui/review/detail-renderer.js +1 -1
- package/src/ui/review/mwr.js +1 -1
- package/src/ui/review/widgets.js +0 -1
- package/src/ui/rfi-detail-renderer.js +1 -1
- package/src/ui/settings/home.js +1 -1
- package/src/ui/settings/review.js +0 -1
- package/src/ui/settings/teams.js +1 -1
- package/src/ui/spacing-system.js +1 -1
package/package.json
CHANGED
|
@@ -4,19 +4,12 @@ import { google } from 'googleapis';
|
|
|
4
4
|
const log = createLogger('[GoogleAuth]');
|
|
5
5
|
import { buildConfig } from '../config/env.js';
|
|
6
6
|
import fs from 'fs';
|
|
7
|
-
import path from 'path';
|
|
8
7
|
|
|
9
8
|
let _oauth2Client = null;
|
|
10
9
|
let _jwtClient = null;
|
|
11
10
|
|
|
12
11
|
export function initGoogleAuth(config = null) {
|
|
13
12
|
const cfg = config || buildConfig();
|
|
14
|
-
const scopes = [
|
|
15
|
-
'https://www.googleapis.com/auth/userinfo.email',
|
|
16
|
-
'https://www.googleapis.com/auth/userinfo.profile',
|
|
17
|
-
'https://www.googleapis.com/auth/drive',
|
|
18
|
-
'https://www.googleapis.com/auth/gmail.send',
|
|
19
|
-
];
|
|
20
13
|
|
|
21
14
|
// OAuth2 client for web flow
|
|
22
15
|
_oauth2Client = new google.auth.OAuth2(
|
|
@@ -4,8 +4,6 @@ import path from 'path';
|
|
|
4
4
|
import { getJWTClient, getOAuth2Client } from './google-auth.js';
|
|
5
5
|
import { buildConfig } from '../config/env.js';
|
|
6
6
|
|
|
7
|
-
const DRIVE_SCOPES = ['https://www.googleapis.com/auth/drive'];
|
|
8
|
-
|
|
9
7
|
export function getDriveClient(user = null) {
|
|
10
8
|
let client;
|
|
11
9
|
|
|
@@ -26,7 +24,7 @@ export function getDriveClient(user = null) {
|
|
|
26
24
|
export async function uploadFile(filePath, name, options = {}, user = null) {
|
|
27
25
|
const drive = getDriveClient(user);
|
|
28
26
|
const absPath = path.resolve(filePath);
|
|
29
|
-
|
|
27
|
+
fs.statSync(absPath);
|
|
30
28
|
|
|
31
29
|
const res = await drive.files.create({
|
|
32
30
|
requestBody: {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createCrudHandlers } from '@/lib/crud-factory';
|
|
2
2
|
import { setCurrentRequest } from '@/engine.server';
|
|
3
3
|
|
|
4
|
-
function wrapHandler(
|
|
4
|
+
function wrapHandler(_method) {
|
|
5
5
|
return async (request, context) => {
|
|
6
6
|
setCurrentRequest(request);
|
|
7
7
|
const params = await context.params;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from '@/lib/next-shim';
|
|
2
2
|
import { requireAuth } from '@/lib/auth-middleware';
|
|
3
|
-
import { searchLogs,
|
|
3
|
+
import { searchLogs, rotateLogsOlderThan } from '@/lib/audit-logger-enhanced';
|
|
4
4
|
import { withErrorHandler } from '@/lib/errors';
|
|
5
5
|
|
|
6
6
|
export const GET = withErrorHandler(async (request) => {
|
|
@@ -33,7 +33,6 @@ export async function GET(request) {
|
|
|
33
33
|
const search = searchParams.get('search');
|
|
34
34
|
const startDate = searchParams.get('start_date');
|
|
35
35
|
const endDate = searchParams.get('end_date');
|
|
36
|
-
const action = searchParams.get('action');
|
|
37
36
|
const limit = parseInt(searchParams.get('limit') || '100', 10);
|
|
38
37
|
const offset = parseInt(searchParams.get('offset') || '0', 10);
|
|
39
38
|
const format = searchParams.get('format');
|
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import { requireAuth
|
|
1
|
+
import { requireAuth } from '@/lib/auth-middleware';
|
|
2
2
|
import { getAuditHistory, getActionStats, getUserStats } from '@/lib/busybase/audit-reads';
|
|
3
|
-
import { getSpec } from '@/config/spec-helpers';
|
|
4
3
|
import { paginated, ok } from '@/lib/response-formatter';
|
|
5
4
|
import { withErrorHandler, AppError } from '@/lib/errors';
|
|
6
5
|
import { HTTP } from '@/config/constants';
|
|
7
|
-
import { parse as parseQuery } from '@/lib/query-string-adapter';
|
|
8
6
|
|
|
9
7
|
export const GET = withErrorHandler(async (request) => {
|
|
10
|
-
|
|
8
|
+
await requireAuth();
|
|
11
9
|
const url = new URL(request.url);
|
|
12
10
|
|
|
13
11
|
const entityType = url.searchParams.get('entityType');
|
|
@@ -2,7 +2,7 @@ import { generateToken } from '@/lib/csrf-protection';
|
|
|
2
2
|
import { ok } from '@/lib/response-formatter';
|
|
3
3
|
import { withErrorHandler } from '@/lib/errors';
|
|
4
4
|
|
|
5
|
-
export const GET = withErrorHandler(async (
|
|
5
|
+
export const GET = withErrorHandler(async (_request) => {
|
|
6
6
|
const token = generateToken();
|
|
7
7
|
return ok({ csrfToken: token });
|
|
8
8
|
}, 'CSRF:GetToken');
|
|
@@ -6,7 +6,7 @@ import { ok } from '@/lib/response-formatter';
|
|
|
6
6
|
import { withErrorHandler } from '@/lib/errors';
|
|
7
7
|
import { getConfigEngine } from '@/lib/config-generator-engine';
|
|
8
8
|
|
|
9
|
-
export const GET = withErrorHandler(async (
|
|
9
|
+
export const GET = withErrorHandler(async (_request) => {
|
|
10
10
|
await getConfigEngine();
|
|
11
11
|
const domainLoader = getDomainLoader();
|
|
12
12
|
const validDomains = domainLoader.getValidDomains();
|
package/src/cli.js
CHANGED
|
@@ -135,7 +135,6 @@ export function buildNavigation(configEngine, user = null) {
|
|
|
135
135
|
// Filter by user permissions if provided
|
|
136
136
|
if (user && configEngine.getRolePermissions) {
|
|
137
137
|
return items.filter(item => {
|
|
138
|
-
const spec = configEngine.generateEntitySpec(item.name);
|
|
139
138
|
const perms = configEngine.getRolePermissions(user.role, item.name);
|
|
140
139
|
return perms.includes('list') || perms.includes('view') || perms.includes('all');
|
|
141
140
|
});
|
package/src/engine.server.js
CHANGED
|
@@ -104,7 +104,7 @@ export async function createSession(userId) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export async function invalidateSession() {
|
|
107
|
-
|
|
107
|
+
getLucia();
|
|
108
108
|
// Simplified - caller handles cookie clearing
|
|
109
109
|
// In full app this uses @lucia-auth/adapter-sqlite's cookie management
|
|
110
110
|
}
|
package/src/lib/alert-manager.js
CHANGED
|
@@ -22,7 +22,7 @@ function checkErrorRate(errorMetrics) {
|
|
|
22
22
|
const window = thresholds.errorRate.window
|
|
23
23
|
let recentErrors = 0
|
|
24
24
|
|
|
25
|
-
for (const [
|
|
25
|
+
for (const [, data] of Object.entries(errorMetrics.byEndpoint)) {
|
|
26
26
|
const recent = (data.recent || []).filter(e => (now - e.ts) < window)
|
|
27
27
|
recentErrors += recent.length
|
|
28
28
|
}
|
package/src/lib/api-helpers.js
CHANGED
|
@@ -30,7 +30,7 @@ export const ok = (data) => NextResponse.json(withMetadata(data, HTTP.OK, 'succe
|
|
|
30
30
|
export const created = (data) => NextResponse.json(withMetadata(data, HTTP.CREATED, 'created'), { status: HTTP.CREATED });
|
|
31
31
|
export const notFound = (entity = 'Resource') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.notFound(entity) }, HTTP.NOT_FOUND, 'error'), { status: HTTP.NOT_FOUND });
|
|
32
32
|
export const badRequest = (reason = 'invalid data') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.invalidRequest(reason) }, HTTP.BAD_REQUEST, 'error'), { status: HTTP.BAD_REQUEST });
|
|
33
|
-
export const unauthorized = (
|
|
33
|
+
export const unauthorized = (_action = 'perform this action') => NextResponse.json(withMetadata({ error: ERROR_MESSAGES.permission.denied }, HTTP.FORBIDDEN, 'error'), { status: HTTP.FORBIDDEN });
|
|
34
34
|
export const serverError = (msg = null) => NextResponse.json(withMetadata({ error: msg || ERROR_MESSAGES.operationFailed('server operation') }, HTTP.INTERNAL_ERROR, 'error'), { status: HTTP.INTERNAL_ERROR });
|
|
35
35
|
|
|
36
36
|
export async function withEntityAccess(entity, action, handler) {
|
|
@@ -23,7 +23,7 @@ export function validateOAuthProvider(provider) {
|
|
|
23
23
|
return { valid: true };
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export async function setOAuthCookie(name, value
|
|
26
|
+
export async function setOAuthCookie(name, value) {
|
|
27
27
|
const timestamp = Date.now();
|
|
28
28
|
const key = `oauth-${timestamp}-${Math.random().toString(36).substring(7)}`;
|
|
29
29
|
const expiresAt = timestamp + (SESSION.cookieMaxAge * 1000);
|
|
@@ -11,7 +11,7 @@ export class BusinessRulesEngine {
|
|
|
11
11
|
return rule && !this.isDuplicateEngagement(engagement);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
this.registerRule('daysOutstandingNotificationNeeded', (
|
|
14
|
+
this.registerRule('daysOutstandingNotificationNeeded', (_rfi) => {
|
|
15
15
|
return this.businessRules.rfiDaysOutstanding.enabled;
|
|
16
16
|
});
|
|
17
17
|
|
|
@@ -23,7 +23,7 @@ export class BusinessRulesEngine {
|
|
|
23
23
|
return true;
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
-
this.registerRule('clientCanRate', (user,
|
|
26
|
+
this.registerRule('clientCanRate', (user, _engagement) => {
|
|
27
27
|
return user.type === 'client' && user.role === 'client_admin';
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -66,7 +66,7 @@ export class BusinessRulesEngine {
|
|
|
66
66
|
return this.businessRules.recreationRules.engagement[interval];
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
isDuplicateEngagement(
|
|
69
|
+
isDuplicateEngagement(_engagement) {
|
|
70
70
|
return false;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -32,7 +32,7 @@ export class BusyBaseAdapter {
|
|
|
32
32
|
this._client = this._embedded;
|
|
33
33
|
logger.info('Embedded mode initialized', { dir: this.config.dir });
|
|
34
34
|
} else {
|
|
35
|
-
const { default: BB
|
|
35
|
+
const { default: BB } = await import('busybase');
|
|
36
36
|
this._client = BB(this.config.url, this.config.project);
|
|
37
37
|
logger.info('Remote mode initialized', { url: this.config.url });
|
|
38
38
|
}
|
|
@@ -106,7 +106,7 @@ export class BusyBaseAdapter {
|
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
async unsubscribeAll() {
|
|
109
|
-
for (const [
|
|
109
|
+
for (const [, { channel }] of _realtimeChannels.entries()) {
|
|
110
110
|
channel.unsubscribe();
|
|
111
111
|
}
|
|
112
112
|
_realtimeChannels.clear();
|
|
@@ -271,7 +271,7 @@ export async function create(entity, data, user) {
|
|
|
271
271
|
// unconditional-write behaviour, unchanged for every existing caller; _version
|
|
272
272
|
// is added as a new column, ignored by every reader that doesn't ask for it.
|
|
273
273
|
export async function update(entity, id, data, opts = {}) {
|
|
274
|
-
|
|
274
|
+
specOf(entity);
|
|
275
275
|
const tbl = tableName(entity);
|
|
276
276
|
|
|
277
277
|
const existing = await get(entity, id);
|
|
@@ -12,9 +12,7 @@
|
|
|
12
12
|
// as of this writing, vs. workflow-engine.js's 2 and
|
|
13
13
|
// xstate-workflow-engine.js's 2 debug/test-only importers) -- this is live,
|
|
14
14
|
// central infrastructure, not a candidate for deprecation.
|
|
15
|
-
import { load as yamlLoad } from 'js-yaml';
|
|
16
15
|
import { LRUCache, deepFreeze, deepClone, recursiveResolve } from './config-helpers.js';
|
|
17
|
-
import { generateFieldsFromOverrides, ensureFieldLabels } from './config-field-helpers.js';
|
|
18
16
|
|
|
19
17
|
export class ConfigGeneratorEngine {
|
|
20
18
|
constructor(masterConfig) {
|
|
@@ -9,7 +9,7 @@ const ACTION_FIELD_MAPS = {
|
|
|
9
9
|
respond: (data, user) => ({ status: 'responded', response: data.response, responded_at: now(), responded_by: user.id }),
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
export function resolveActionUpdate(action, data, user,
|
|
12
|
+
export function resolveActionUpdate(action, data, user, _record) {
|
|
13
13
|
const mapped = ACTION_FIELD_MAPS[action];
|
|
14
14
|
if (typeof mapped === 'function') return mapped(data, user);
|
|
15
15
|
if (mapped) {
|
package/src/lib/crud-handlers.js
CHANGED
|
@@ -3,10 +3,10 @@ import { get, listWithPagination, searchWithPagination, create, update, remove }
|
|
|
3
3
|
|
|
4
4
|
const log = createLogger('[CRUD]');
|
|
5
5
|
import { validateEntity, validateUpdate, sanitizeData } from './validation/index.js';
|
|
6
|
-
import { requirePermission
|
|
6
|
+
import { requirePermission } from './auth-middleware.js';
|
|
7
7
|
import { executeHook } from './hook-engine.js';
|
|
8
8
|
import { AppError, NotFoundError, ValidationError } from './errors/index.js';
|
|
9
|
-
import { ok, created, paginated, noContent
|
|
9
|
+
import { ok, created, paginated, noContent } from './response-formatter.js';
|
|
10
10
|
import { HTTP } from '../config/constants.js';
|
|
11
11
|
import { permissionService } from '../services/permission.service.js';
|
|
12
12
|
import { parse as parseQuery } from './query-string-adapter.js';
|
package/src/lib/errors/wrap.js
CHANGED
|
@@ -48,7 +48,7 @@ export function wrap(fn, options = {}) {
|
|
|
48
48
|
// retryWithBackoff lives here (not recovery.js) because wrap() itself needs it
|
|
49
49
|
// with no circular dependency; recovery.js imports it back from here.
|
|
50
50
|
export async function retryWithBackoff(fn, options = {}) {
|
|
51
|
-
const { maxAttempts = 3, delay = 1000, backoff = 2
|
|
51
|
+
const { maxAttempts = 3, delay = 1000, backoff = 2 } = options;
|
|
52
52
|
|
|
53
53
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
54
54
|
try {
|
package/src/lib/events-engine.js
CHANGED
|
@@ -175,7 +175,7 @@ export const registerEntityHandlers = () => {
|
|
|
175
175
|
create('removed_highlight', { review_id: highlight.review_id, original_id: highlight.id, highlight_data: JSON.stringify(highlight) }, user);
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
hookEngine.on('collaborator:afterCreate', async (collaborator,
|
|
178
|
+
hookEngine.on('collaborator:afterCreate', async (collaborator, _user) => {
|
|
179
179
|
const review = get('review', collaborator.review_id);
|
|
180
180
|
const collabUser = get('user', collaborator.user_id);
|
|
181
181
|
if (review && collabUser) {
|
|
@@ -20,13 +20,13 @@ export function getFieldNames(spec) {
|
|
|
20
20
|
|
|
21
21
|
export function getEditableFields(spec) {
|
|
22
22
|
return Object.entries(spec.fields || {})
|
|
23
|
-
.filter(([
|
|
23
|
+
.filter(([, field]) => !field.readonly && !field.auto && !field.auto_generate)
|
|
24
24
|
.map(([key]) => key);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export function getFieldsByType(spec, type) {
|
|
28
28
|
return Object.entries(spec.fields || {})
|
|
29
|
-
.filter(([
|
|
29
|
+
.filter(([, field]) => field.type === type);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function fieldQuery(spec, predicates, options = {}) {
|
package/src/lib/next-shim.js
CHANGED
|
@@ -262,9 +262,9 @@ export async function cookies() {
|
|
|
262
262
|
|
|
263
263
|
export function headers() {
|
|
264
264
|
return {
|
|
265
|
-
get: (
|
|
265
|
+
get: (_name) => null,
|
|
266
266
|
getSetCookie: () => [],
|
|
267
|
-
has: (
|
|
267
|
+
has: (_name) => false,
|
|
268
268
|
entries: () => [],
|
|
269
269
|
};
|
|
270
270
|
}
|
package/src/lib/perf.js
CHANGED
|
@@ -22,7 +22,6 @@ const logger = createLogger('[Perf]');
|
|
|
22
22
|
const _profiles = new Map();
|
|
23
23
|
const _alerts = [];
|
|
24
24
|
const MAX_ALERTS = 100;
|
|
25
|
-
const BASELINE_WINDOW_MS = 3600000;
|
|
26
25
|
|
|
27
26
|
const DEFAULT_THRESHOLDS = {
|
|
28
27
|
'http.request': 500,
|
|
@@ -119,7 +118,6 @@ export class PerfProfiler {
|
|
|
119
118
|
if (!profile) return null;
|
|
120
119
|
|
|
121
120
|
const durations = profile.durations.map(d => d.durationMs).sort((a, b) => a - b);
|
|
122
|
-
const count = durations.length;
|
|
123
121
|
|
|
124
122
|
return {
|
|
125
123
|
operation: profile.operation,
|
package/src/lib/request-trace.js
CHANGED
|
@@ -6,11 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { tracer } from './tracing.js';
|
|
8
8
|
import { perfProfiler } from './perf.js';
|
|
9
|
-
import { createLogger } from './logger.js';
|
|
10
9
|
import { recordRequest, recordError } from './metrics-collector.js';
|
|
11
10
|
|
|
12
|
-
const logger = createLogger('[RequestTracing]');
|
|
13
|
-
|
|
14
11
|
// ---------------------------------------------------------------------------
|
|
15
12
|
// From request-tracing.js
|
|
16
13
|
// ---------------------------------------------------------------------------
|
|
@@ -223,7 +220,7 @@ export default withTracing;
|
|
|
223
220
|
// ---------------------------------------------------------------------------
|
|
224
221
|
// From request-tracker.js
|
|
225
222
|
// ---------------------------------------------------------------------------
|
|
226
|
-
function wrapHandler(handler,
|
|
223
|
+
function wrapHandler(handler, _metadata = {}) {
|
|
227
224
|
return async (request, context) => {
|
|
228
225
|
const start = process.hrtime.bigint();
|
|
229
226
|
const method = request.method;
|
|
@@ -21,7 +21,6 @@ export function createStagePipeline(options = {}) {
|
|
|
21
21
|
stages.forEach((stage, idx) => {
|
|
22
22
|
const isComplete = idx < currentStageIndex;
|
|
23
23
|
const isCurrent = idx === currentStageIndex;
|
|
24
|
-
const isUpcoming = idx > currentStageIndex;
|
|
25
24
|
|
|
26
25
|
const stageItem = document.createElement('div');
|
|
27
26
|
stageItem.className = `stage-item stage-${isComplete ? 'complete' : isCurrent ? 'current' : 'upcoming'}`;
|
package/src/lib/static-server.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'url';
|
|
|
5
5
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
6
|
const ROOT = path.join(__dirname, '../..');
|
|
7
7
|
|
|
8
|
-
export function serveStatic(pathname, req, res, compress, getCacheHeaders,
|
|
8
|
+
export function serveStatic(pathname, req, res, compress, getCacheHeaders, _loadModule) {
|
|
9
9
|
const acceptEncoding = req.headers['accept-encoding'] || '';
|
|
10
10
|
|
|
11
11
|
if (pathname === '/favicon.ico') {
|
package/src/lib/tracing.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { validateEntity, validateUpdate, hasErrors } from '@/lib/validation/entity-validators';
|
|
11
11
|
|
|
12
12
|
export async function validateRequest(request, options = {}) {
|
|
13
|
-
const { requireCSRF = true, rateLimit = { max: 100, window: 60000 }
|
|
13
|
+
const { requireCSRF = true, rateLimit = { max: 100, window: 60000 } } = options;
|
|
14
14
|
|
|
15
15
|
const ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown';
|
|
16
16
|
const sessionId = request.headers.get('cookie')?.match(/session=([^;]+)/)?.[1] || ip;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// does NOT currently supersede this file; if anything the reverse holds (this file
|
|
19
19
|
// is live, that one is dormant/debug-only). Re-check import sites before treating
|
|
20
20
|
// either as the "old" one.
|
|
21
|
-
import { get
|
|
21
|
+
import { get } from './busybase/store.js';
|
|
22
22
|
import { getConfigEngineSync } from './config-generator-engine.js';
|
|
23
23
|
import { executeHook } from './hook-engine.js';
|
|
24
24
|
import { AppError } from './errors/index.js';
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// src/lib/events-engine.js and re-exported by src/index.js's Thatcher class).
|
|
15
15
|
// This file does NOT supersede workflow-engine.js; treat it as a dormant/
|
|
16
16
|
// debug-only alternate implementation until a real call site adopts it.
|
|
17
|
-
import { createMachine, createActor, assign
|
|
17
|
+
import { createMachine, createActor, assign } from 'xstate';
|
|
18
18
|
import { getConfigEngineSync } from './config-generator-engine.js';
|
|
19
19
|
import { hookEngine } from './hook-engine.js';
|
|
20
20
|
import { AppError } from './errors/index.js';
|
package/src/server/server.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import http from 'http';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import fs from 'fs';
|
|
4
|
-
import { fileURLToPath
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
5
|
import { createLogger } from '../lib/logger.js';
|
|
6
|
-
import * as thatcherLib from '../index.js';
|
|
7
6
|
|
|
8
7
|
const log = createLogger('[Server]');
|
|
9
8
|
const pageLog = createLogger('[Page]');
|
|
@@ -13,8 +12,7 @@ const apiLog = createLogger('[API]');
|
|
|
13
12
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
13
|
|
|
15
14
|
export function createServer(options) {
|
|
16
|
-
const { thatcher,
|
|
17
|
-
const PORT = port || 3000;
|
|
15
|
+
const { thatcher, configEngine } = options;
|
|
18
16
|
const SERVER_START = Date.now();
|
|
19
17
|
|
|
20
18
|
let systemInitialized = false;
|
|
@@ -50,7 +48,6 @@ export function createServer(options) {
|
|
|
50
48
|
};
|
|
51
49
|
|
|
52
50
|
const server = http.createServer(async (req, res) => {
|
|
53
|
-
const t0 = Date.now();
|
|
54
51
|
globalThis.__debug__.activeRequests.count++;
|
|
55
52
|
|
|
56
53
|
try {
|
|
@@ -98,7 +95,7 @@ export function createServer(options) {
|
|
|
98
95
|
}
|
|
99
96
|
|
|
100
97
|
// Static file serving (simplified)
|
|
101
|
-
if (serveStaticFile(pathname, req, res
|
|
98
|
+
if (serveStaticFile(pathname, req, res)) {
|
|
102
99
|
return;
|
|
103
100
|
}
|
|
104
101
|
|
|
@@ -135,7 +132,7 @@ export function createServer(options) {
|
|
|
135
132
|
return server;
|
|
136
133
|
}
|
|
137
134
|
|
|
138
|
-
async function serveStaticFile(pathname, req, res
|
|
135
|
+
async function serveStaticFile(pathname, req, res) {
|
|
139
136
|
if (pathname === '/' || pathname === '/index.html') {
|
|
140
137
|
// Could serve SPA
|
|
141
138
|
return false;
|
|
@@ -220,9 +217,8 @@ async function handleGenericCrud(req, res, entity, id, action, thatcher, configE
|
|
|
220
217
|
const { getConfigEngineSync } = await import('../lib/config-generator-engine.js');
|
|
221
218
|
configEngine = getConfigEngineSync();
|
|
222
219
|
}
|
|
223
|
-
let spec;
|
|
224
220
|
try {
|
|
225
|
-
|
|
221
|
+
configEngine.generateEntitySpec(entity);
|
|
226
222
|
} catch (e) {
|
|
227
223
|
res.writeHead(404);
|
|
228
224
|
res.end(JSON.stringify({ error: `Entity '${entity}' not found` }));
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// Extracted from moonlanding/src/services/collaborator-role.service.js
|
|
2
2
|
|
|
3
3
|
import { list, get, update, create } from '../lib/busybase/store.js';
|
|
4
|
-
import { AppError } from '../lib/errors/index.js';
|
|
5
|
-
import { HTTP } from '../config/constants.js';
|
|
6
4
|
|
|
7
5
|
const COLLABORATOR_ROLE_PERMISSIONS = {
|
|
8
6
|
viewer: ['view', 'view_highlights', 'view_pdfs'],
|
|
@@ -78,7 +76,7 @@ export function getCollaboratorRolePermissions(roleType) {
|
|
|
78
76
|
return COLLABORATOR_ROLE_PERMISSIONS[roleType] || [];
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
export function canAssignRole(userRole,
|
|
79
|
+
export function canAssignRole(userRole, _targetRoleType) {
|
|
82
80
|
return ['partner', 'manager'].includes(userRole);
|
|
83
81
|
}
|
|
84
82
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sendEmail } from './email-sender.js';
|
|
2
2
|
import { getConfigEngineSync } from '../lib/config-generator-engine.js';
|
|
3
3
|
import { executeHook } from '../lib/hook-engine.js';
|
|
4
4
|
import { createLogger } from '../lib/logger.js';
|
|
@@ -65,7 +65,7 @@ export async function sendNotification(type, userId, context = {}, options = {})
|
|
|
65
65
|
return notification;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export async function markNotificationRead(notificationId,
|
|
68
|
+
export async function markNotificationRead(notificationId, _userId) {
|
|
69
69
|
const { update } = await import('../lib/busybase/store.js');
|
|
70
70
|
return update('notification', notificationId, {
|
|
71
71
|
read_at: Math.floor(Date.now() / 1000),
|
|
@@ -125,7 +125,7 @@ async function resolveRecipients(recipients, context) {
|
|
|
125
125
|
return [];
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function templateNameForType(
|
|
128
|
+
function templateNameForType(_type) {
|
|
129
129
|
// events-engine.js passes descriptive template names (e.g.
|
|
130
130
|
// 'engagement_info_gathering'); the built-in getTemplates() set in
|
|
131
131
|
// email-sender.js is generic ('notification', 'invitation'), so an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Adapted from moonlanding/src/services/permission.service.js
|
|
2
2
|
|
|
3
|
-
import { getCollaboratorRole
|
|
3
|
+
import { getCollaboratorRole } from '../services/collaborator-role.service.js';
|
|
4
4
|
import { PermissionError } from '../lib/errors/index.js';
|
|
5
5
|
import { getConfigEngineSync } from '../lib/config-generator-engine.js';
|
|
6
6
|
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { h } from '@/ui/webjsx.js'
|
|
2
|
-
import { TOAST_SCRIPT } from '@/ui/render-helpers.js'
|
|
3
|
-
|
|
4
1
|
export function dataGridAdvanced(config) {
|
|
5
2
|
const { columns = [], data = [], groupBy = null, sortable = true, filterable = true, expandable = false, detailRenderer = null } = config
|
|
6
3
|
const gridId = 'dg-' + Math.random().toString(36).slice(2, 8)
|
package/src/ui/auth-pages.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { generateHtml } from '@/ui/layout.js'
|
|
2
2
|
import { nav as navFn } from '@/ui/layout.js'
|
|
3
|
-
import { AUTH_CSS, GOOGLE_SVG, WARN_SVG,
|
|
3
|
+
import { AUTH_CSS, GOOGLE_SVG, WARN_SVG, authLogoHtml } from '@/ui/auth-styles.js'
|
|
4
4
|
import { esc } from '@/ui/render-helpers.js'
|
|
5
5
|
|
|
6
6
|
export function renderLogin(error = null, hasGoogleAuth = false) {
|
|
@@ -32,7 +32,7 @@ export function renderComponent(componentId, props = {}) {
|
|
|
32
32
|
const regex = new RegExp(`{{${key}}}`, 'g');
|
|
33
33
|
if (value && typeof value === 'object') {
|
|
34
34
|
if (Array.isArray(value)) {
|
|
35
|
-
html = html.replace(regex, value.map(v => renderItem(v
|
|
35
|
+
html = html.replace(regex, value.map(v => renderItem(v)).join(''));
|
|
36
36
|
} else {
|
|
37
37
|
html = html.replace(regex, esc(JSON.stringify(value)));
|
|
38
38
|
}
|
|
@@ -49,7 +49,7 @@ export function renderComponent(componentId, props = {}) {
|
|
|
49
49
|
return `<${element}${cssClass}>${html}</${element}>`;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
function renderItem(item
|
|
52
|
+
function renderItem(item) {
|
|
53
53
|
if (typeof item === 'string') return esc(item);
|
|
54
54
|
if (typeof item === 'object' && item.name && item.color) {
|
|
55
55
|
const bg = safeColor(item.color);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { page } from '@/ui/layout.js'
|
|
2
2
|
import { getQuickActions } from '@/ui/permissions-ui.js'
|
|
3
3
|
import { esc, stagePill, statusPill, TABLE_SCRIPT, emptyRow } from '@/ui/render-helpers.js'
|
|
4
|
-
import { SPACING
|
|
4
|
+
import { SPACING } from '@/ui/spacing-system.js'
|
|
5
5
|
|
|
6
6
|
export function renderDashboard(user, stats = {}) {
|
|
7
7
|
const isClerk = user?.role === 'clerk';
|
package/src/ui/dialog-engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getConfigEngineSync } from '@/lib/config-generator-engine';
|
|
2
2
|
import { aria, role } from '@/lib/accessibility';
|
|
3
3
|
|
|
4
|
-
export function renderDialog(dialogId,
|
|
4
|
+
export function renderDialog(dialogId, _context = {}) {
|
|
5
5
|
const config = getConfigEngineSync().getConfig();
|
|
6
6
|
const dialogConfig = config.dialogs?.[dialogId];
|
|
7
7
|
|
|
@@ -10,15 +10,15 @@ export function renderDialog(dialogId, context = {}) {
|
|
|
10
10
|
return '';
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const { title, fields = [], actions = [], width = '640px'
|
|
14
|
-
const dialogHtml = generateDialogHtml(dialogId, title, fields, actions, width
|
|
13
|
+
const { title, fields = [], actions = [], width = '640px' } = dialogConfig;
|
|
14
|
+
const dialogHtml = generateDialogHtml(dialogId, title, fields, actions, width);
|
|
15
15
|
|
|
16
16
|
return dialogHtml;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function generateDialogHtml(dialogId, title, fields, actions, width
|
|
19
|
+
function generateDialogHtml(dialogId, title, fields, actions, width) {
|
|
20
20
|
const titleId = `${dialogId}-title`;
|
|
21
|
-
const fieldHtml = fields.map(f => generateFieldHtml(f, dialogId
|
|
21
|
+
const fieldHtml = fields.map(f => generateFieldHtml(f, dialogId)).join('');
|
|
22
22
|
const actionButtons = (actions || []).map(a => {
|
|
23
23
|
if (a.type === 'cancel') {
|
|
24
24
|
return `<button type="button" class="btn btn-ghost btn-sm" ${aria.label('Cancel dialog')} data-action="closeDialog" data-args='["${dialogId}"]'>${a.label || 'Cancel'}</button>`;
|
|
@@ -66,8 +66,8 @@ function generateDialogHtml(dialogId, title, fields, actions, width, context, on
|
|
|
66
66
|
`;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
function generateFieldHtml(field, dialogId
|
|
70
|
-
const { name, label, type, required, placeholder, options
|
|
69
|
+
function generateFieldHtml(field, dialogId) {
|
|
70
|
+
const { name, label, type, required, placeholder, options } = field;
|
|
71
71
|
const fieldId = `${dialogId}-${name}`;
|
|
72
72
|
const requiredAttr = required ? 'required' : '';
|
|
73
73
|
const ariaRequired = required ? aria.required() : '';
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { statusLabel } from '@/ui/renderer.js';
|
|
2
1
|
import { createDialog } from '@/ui/dialog-factory.js';
|
|
3
|
-
import { esc } from '@/ui/render-helpers.js';
|
|
4
2
|
|
|
5
3
|
export function engagementFileSearchDialog(engagementId) {
|
|
6
4
|
const body = `<div class="modal-form-group"><label for="efs-query" class="sr-only">Search files</label><input type="text" id="efs-query" class="input input-bordered w-full" placeholder="Search files..." aria-label="Search engagement files" oninput="efsSearch()"/></div><div id="efs-results" class="flex flex-col gap-2" style="max-height:400px;overflow:auto"></div>`;
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { page, confirmDialog, dataTable } from '@/ui/layout.js'
|
|
1
|
+
import { page, dataTable } from '@/ui/layout.js'
|
|
3
2
|
import { fmtVal, TOAST_SCRIPT, esc } from '@/ui/render-helpers.js'
|
|
4
3
|
import { canCreate, canEdit, canDelete } from '@/ui/permissions-ui.js'
|
|
5
4
|
|
|
@@ -7,7 +6,7 @@ export function renderEntityList(entityName, items, spec, user, options = {}) {
|
|
|
7
6
|
const label = spec?.labelPlural || spec?.label || entityName
|
|
8
7
|
const fields = spec?.fields || {}
|
|
9
8
|
const { groupBy = null } = options
|
|
10
|
-
let listFields = Object.entries(fields).filter(([
|
|
9
|
+
let listFields = Object.entries(fields).filter(([, f]) => f.list).slice(0, 5)
|
|
11
10
|
if (!listFields.length) listFields = Object.entries(fields).filter(([k]) => !['created_by', 'updated_by'].includes(k)).slice(0, 6)
|
|
12
11
|
if (!listFields.length && items.length > 0) listFields = Object.keys(items[0]).filter(k => !['created_by', 'updated_by'].includes(k)).slice(0, 5).map(k => [k, { label: k }])
|
|
13
12
|
const headers = listFields.map(([k, f]) => `<th>${esc(f?.label || k)}</th>`).join('') + '<th>Actions</th>'
|
|
@@ -260,7 +260,7 @@ const eventDelegation = {
|
|
|
260
260
|
|
|
261
261
|
navigate(path) { window.location = path; },
|
|
262
262
|
|
|
263
|
-
toggle(elementId,
|
|
263
|
+
toggle(elementId, _attr = 'checked') {
|
|
264
264
|
const el = document.getElementById(elementId);
|
|
265
265
|
if (el?.type === 'checkbox') el.checked = !el.checked;
|
|
266
266
|
else el.classList.toggle('active');
|
package/src/ui/file-dialogs.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { statusLabel } from '@/ui/renderer.js';
|
|
2
|
-
|
|
3
1
|
// Inline HTML-escape for client-script innerHTML sinks (file names are DB-sourced).
|
|
4
2
|
const FD_ESC = `function fdEsc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''')}`;
|
|
5
3
|
|
package/src/ui/layout.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/* eslint-disable no-useless-escape -- this file embeds raw client <script> text inside a template literal; ESLint parses embedded regex escapes like \d as literal template text and misflags them */
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { TOAST_SCRIPT, AVATAR_COLORS, esc } from '@/ui/render-helpers.js'
|
|
2
|
+
import { isPartner, isClerk } from '@/ui/permissions-ui.js'
|
|
3
|
+
import { AVATAR_COLORS, esc } from '@/ui/render-helpers.js'
|
|
5
4
|
import { FETCH_JSON_SCRIPT } from '@/ui/fetch-json.js'
|
|
6
5
|
import { aria, role, skipLink, liveRegion } from '@/lib/accessibility'
|
|
7
6
|
|
|
@@ -17,7 +16,7 @@ function withAppName(title) {
|
|
|
17
16
|
return base === appName ? base : `${base} | ${appName}`
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
export function generateHtml(title, bodyContent, scripts = []
|
|
19
|
+
export function generateHtml(title, bodyContent, scripts = []) {
|
|
21
20
|
title = withAppName(title)
|
|
22
21
|
const scriptTags = scripts.map(s =>
|
|
23
22
|
typeof s === 'string' ? `<script>${s.replace(/<\/script/gi, '<\\/script').replace(/<!--/g, '<\\!--')}</script>` : `<script type="module" src="${s.src}"></script>`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { list, get } from '@/lib/busybase/store.js';
|
|
2
|
-
import { renderAuditDashboard, renderSystemHealth, renderAccessDenied, generateHtml
|
|
2
|
+
import { renderAuditDashboard, renderSystemHealth, renderAccessDenied, generateHtml } from '@/ui/renderer.js';
|
|
3
3
|
import { renderSettingsHome, renderSettingsSystem, renderSettingsUsers } from '@/ui/settings/home.js';
|
|
4
|
-
import {
|
|
4
|
+
import { renderSettingsRfiSections } from '@/ui/settings/teams.js';
|
|
5
5
|
import { renderSettingsTemplates, renderSettingsChecklists, renderSettingsEntityTypes, renderSettingsEngagementTypes, renderSettingsTemplateManage } from '@/ui/settings/templates.js';
|
|
6
6
|
import { renderSettingsNotifications, renderSettingsIntegrations, renderSettingsRecreation, renderSettingsReviewSettings, renderSettingsFileReview, renderSettingsMwrPermissions } from '@/ui/settings/review.js';
|
|
7
7
|
import { renderChecklistsManagement } from '@/ui/checklist-renderer.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { list,
|
|
1
|
+
import { list, count } from '@/lib/busybase/store.js';
|
|
2
2
|
import { createLogger } from '@/lib/logger.js';
|
|
3
3
|
|
|
4
4
|
const log = createLogger('[PageHandlerHelpers]');
|
|
@@ -19,7 +19,7 @@ export function resolveEnumOptions(spec) {
|
|
|
19
19
|
|
|
20
20
|
export async function getRefOptions(spec) {
|
|
21
21
|
const refOptions = {};
|
|
22
|
-
for (const [fieldKey, field] of Object.entries(spec.fields || {}).filter(([
|
|
22
|
+
for (const [fieldKey, field] of Object.entries(spec.fields || {}).filter(([, f]) => f.type === 'ref' && f.ref)) {
|
|
23
23
|
try { refOptions[fieldKey] = (await list(field.ref, {})).map(r => ({ value: r.id, label: r.name || r.title || r.label || r.email || r.id })); }
|
|
24
24
|
catch { refOptions[fieldKey] = []; }
|
|
25
25
|
}
|
|
@@ -31,7 +31,7 @@ export async function getRefOptions(spec) {
|
|
|
31
31
|
// `_display` key exists (falling back to the raw id) without issuing any query.
|
|
32
32
|
export function resolveRefFields(items, spec) {
|
|
33
33
|
try {
|
|
34
|
-
const refFields = Object.entries(spec.fields || {}).filter(([
|
|
34
|
+
const refFields = Object.entries(spec.fields || {}).filter(([, f]) => f.type === 'ref' && f.ref);
|
|
35
35
|
if (!refFields.length) return items;
|
|
36
36
|
return items.map(item => {
|
|
37
37
|
const resolved = { ...item };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { list, get } from '@/lib/busybase/store.js';
|
|
2
|
-
import { canView, canList, isClerk
|
|
2
|
+
import { canView, canList, isClerk } from '@/ui/permissions-ui.js';
|
|
3
3
|
import { renderAccessDenied } from '@/ui/renderer.js';
|
|
4
4
|
import { renderRfiList } from '@/ui/rfi-list-renderer.js';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
package/src/ui/page-handler.js
CHANGED
|
@@ -2,7 +2,7 @@ import { getUser, setCurrentRequest } from '@/engine.server.js';
|
|
|
2
2
|
import { hasGoogleAuth } from '@/config/env.js';
|
|
3
3
|
import { getSpec } from '@/config/spec-helpers.js';
|
|
4
4
|
import { list, get } from '@/lib/busybase/store.js';
|
|
5
|
-
import { renderLogin, renderDashboard,
|
|
5
|
+
import { renderLogin, renderDashboard, renderAccessDenied, renderPasswordReset, renderPasswordResetConfirm, REDIRECT } from '@/ui/renderer.js';
|
|
6
6
|
import { renderClientDashboard, renderClientList } from '@/ui/client-renderer.js';
|
|
7
7
|
import { canList, canView, canCreate, canEdit, isPartner, isClerk, isClientUser, canClientAccessEntity } from '@/ui/permissions-ui.js';
|
|
8
8
|
import { renderEngagementGrid } from '@/ui/engagement-grid-renderer.js';
|
package/src/ui/permissions-ui.js
CHANGED
|
@@ -1,16 +1,5 @@
|
|
|
1
1
|
import { getConfigEngineSync } from '@/lib/config-generator-engine.js';
|
|
2
2
|
|
|
3
|
-
function getRoleHierarchy() {
|
|
4
|
-
const config = getConfigEngineSync();
|
|
5
|
-
const roles = config.getRoles();
|
|
6
|
-
const hierarchy = {};
|
|
7
|
-
let level = 0;
|
|
8
|
-
for (const [roleName] of Object.entries(roles)) {
|
|
9
|
-
hierarchy[roleName] = level++;
|
|
10
|
-
}
|
|
11
|
-
return hierarchy;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
3
|
function getEntityPermissions() {
|
|
15
4
|
const config = getConfigEngineSync();
|
|
16
5
|
const entityNames = config.getAllEntities();
|
|
@@ -40,7 +29,6 @@ function getEntityPermissions() {
|
|
|
40
29
|
|
|
41
30
|
export function canAccess(user, entity, action) {
|
|
42
31
|
if (!user?.role) return false;
|
|
43
|
-
const config = getConfigEngineSync();
|
|
44
32
|
const allPermissions = getEntityPermissions();
|
|
45
33
|
const permissions = allPermissions[entity];
|
|
46
34
|
|
package/src/ui/picker-dialogs.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { h } from '@/ui/webjsx.js'
|
|
2
1
|
import { STAGE_COLORS, TOAST_SCRIPT } from '@/ui/render-helpers.js'
|
|
3
2
|
|
|
4
3
|
const DIALOG_COLORS = ['#B0B0B0','#44BBA4','#FF4141','#7F7EFF','#3b82f6','#f59e0b','#ec4899','#8b5cf6','#ef4444','#22c55e','#06b6d4','#f97316','#84cc16','#e11d48','#14b8a6','#6366f1']
|
|
@@ -2,7 +2,7 @@ import { statusLabel } from '@/ui/renderer.js';
|
|
|
2
2
|
import { page } from '@/ui/layout.js';
|
|
3
3
|
import { emptyRow, esc } from '../render-helpers.js';
|
|
4
4
|
|
|
5
|
-
function reviewSummaryPanel(review, highlights,
|
|
5
|
+
function reviewSummaryPanel(review, highlights, _side) {
|
|
6
6
|
const total = highlights.length;
|
|
7
7
|
const resolved = highlights.filter(h => h.status === 'resolved').length;
|
|
8
8
|
const sts = review.status ? statusLabel(review.status) : '';
|
|
@@ -2,7 +2,7 @@ import { page } from '@/ui/layout.js';
|
|
|
2
2
|
import { reviewZoneNav } from '@/ui/review/zone-nav.js';
|
|
3
3
|
export { reviewZoneNav };
|
|
4
4
|
import { canEdit } from '@/ui/permissions-ui.js';
|
|
5
|
-
import { esc, statusBadge,
|
|
5
|
+
import { esc, statusBadge, icon } from '@/ui/render-helpers.js';
|
|
6
6
|
import { reviewDetailScript } from '@/ui/review/detail-script.js';
|
|
7
7
|
import { highlightRow, collaboratorRow, addCollaboratorDialog } from '@/ui/review/detail-panels.js';
|
|
8
8
|
import { tenderPanelHtml, tenderDialog, linksPanelHtml, linkDialog, sectionsPanelHtml, compareDialogHtml } from '@/ui/review-detail-panels-extra.js';
|
package/src/ui/review/mwr.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { page } from '@/ui/layout.js';
|
|
2
2
|
import { reviewZoneNav } from '@/ui/review/zone-nav.js';
|
|
3
|
-
import { SPACING, renderCard, renderButton,
|
|
3
|
+
import { SPACING, renderCard, renderButton, renderEmptyState, renderStatsRow, renderPageHeader } from '@/ui/spacing-system.js';
|
|
4
4
|
import { icon, esc } from '@/ui/format-helpers.js';
|
|
5
5
|
import { TOAST_SCRIPT } from '@/ui/render-helpers.js';
|
|
6
6
|
|
package/src/ui/review/widgets.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { page } from '@/ui/layout.js';
|
|
2
2
|
import { canEdit, isPartner, isManager, isClientUser } from '@/ui/permissions-ui.js';
|
|
3
|
-
import { esc, statusBadge, TOAST_SCRIPT
|
|
3
|
+
import { esc, statusBadge, TOAST_SCRIPT } from '@/ui/render-helpers.js';
|
|
4
4
|
import { SPACING } from '@/ui/spacing-system.js';
|
|
5
5
|
|
|
6
6
|
const TOAST = TOAST_SCRIPT;
|
package/src/ui/settings/home.js
CHANGED
|
@@ -22,7 +22,7 @@ const SETTINGS_CARDS = [
|
|
|
22
22
|
{ key: 'permissions', icon: 'lock', title: 'Permissions', desc: 'MWR permissions and access control', href: '/admin/settings/permissions' },
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
-
export function renderSettingsHome(user,
|
|
25
|
+
export function renderSettingsHome(user, _config = {}, counts = {}) {
|
|
26
26
|
const cards = SETTINGS_CARDS.map(c => {
|
|
27
27
|
const cnt = c.countKey && counts[c.countKey] !== undefined ? counts[c.countKey] : null;
|
|
28
28
|
const badge = cnt !== null ? `<span class="badge badge-flat-primary text-xs">${cnt.toLocaleString('en-ZA')} items</span>` : '';
|
|
@@ -181,7 +181,6 @@ export function renderSettingsFileReview(user, config = {}, frSettings = {}) {
|
|
|
181
181
|
const fr = config.fileReview || {};
|
|
182
182
|
const reviewFlags = frSettings.review_flags || [];
|
|
183
183
|
const tenderFlags = frSettings.tender_flags || [];
|
|
184
|
-
const flagManagers = frSettings.flags_managers || [];
|
|
185
184
|
const tempAccess = frSettings.temp_review_access_period || 0;
|
|
186
185
|
const toggles = [
|
|
187
186
|
{ id: 'auto_pdf_cache', label: 'Auto-cache PDFs', desc: 'Cache PDF files for faster loading', checked: fr.auto_pdf_cache !== false },
|
package/src/ui/settings/teams.js
CHANGED
|
@@ -12,7 +12,7 @@ function parseMembers(t) {
|
|
|
12
12
|
} catch { return []; }
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function renderSettingsTeams(user, teams = [],
|
|
15
|
+
export function renderSettingsTeams(user, teams = [], _allUsers = []) {
|
|
16
16
|
const editDialog = `<div id="team-dialog" class="modal" style="display:none" role="dialog" aria-modal="true" aria-labelledby="team-dialog-title">
|
|
17
17
|
<div class="modal-overlay" data-dialog-close="team-dialog"></div>
|
|
18
18
|
<div class="modal-content rounded-box max-w-md p-6">
|
package/src/ui/spacing-system.js
CHANGED
|
@@ -93,7 +93,7 @@ export function renderButton(label, opts = {}) {
|
|
|
93
93
|
return `<button type="button"${onclickAttr} class="${btnClass}" ${disabled ? 'disabled' : ''} style="${extraStyle}">${label}</button>`;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
export function renderProgress(value, max = 100,
|
|
96
|
+
export function renderProgress(value, max = 100, _variant = 'success', marginTop = SPACING.md) {
|
|
97
97
|
const pct = max > 0 ? Math.round((value / max) * 100) : 0;
|
|
98
98
|
return `<div style="margin-top:${typeof marginTop === 'string' ? marginTop : SPACING.md}">
|
|
99
99
|
<div style="display:flex;justify-content:space-between;margin-bottom:${SPACING.sm};align-items:center">
|