hazo_admin 0.6.3 → 0.6.4

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/CHANGE_LOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # hazo_admin Changelog
2
2
 
3
+ ## [0.6.4] - 2026-06-26
4
+
5
+ ### Fixed
6
+ - Dist rebuild to include 0.6.2 env panel changes that were missing from the 0.6.3 publish (source changes were present but `npm run build` was not run before publish).
7
+
3
8
  ## [0.6.2] - 2026-06-25
4
9
 
5
10
  ### Added
@@ -10,6 +10,20 @@ export declare function envMigrateJobHandler(job: {
10
10
  }, ctx: {
11
11
  log: (...args: any[]) => void;
12
12
  }): Promise<unknown>;
13
+ export declare const ENV_FILES_SYNC_JOB_TYPE: "env.files-sync";
14
+ export declare function envFilesSyncJobHandler(job: {
15
+ id: string;
16
+ payload: unknown;
17
+ }, ctx: {
18
+ log: (...args: any[]) => void;
19
+ }): Promise<unknown>;
20
+ export declare const ENV_CLEAR_JOB_TYPE: "env.clear";
21
+ export declare function envClearJobHandler(job: {
22
+ id: string;
23
+ payload: unknown;
24
+ }, ctx: {
25
+ log: (...args: any[]) => void;
26
+ }): Promise<unknown>;
13
27
  /**
14
28
  * Register the env.migrate job type with a hazo_jobs worker.
15
29
  */
@@ -1 +1 @@
1
- {"version":3,"file":"env_migrate_handler.d.ts","sourceRoot":"","sources":["../../src/api/env_migrate_handler.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAGrB,eAAO,MAAM,oBAAoB,EAAG,aAAsB,CAAC;AAE3D;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,EACrC,GAAG,EAAE;IAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;CAAE,GACrC,OAAO,CAAC,OAAO,CAAC,CAkClB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IACtC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CACrF,GAAG,IAAI,CAEP"}
1
+ {"version":3,"file":"env_migrate_handler.d.ts","sourceRoot":"","sources":["../../src/api/env_migrate_handler.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAGrB,eAAO,MAAM,oBAAoB,EAAG,aAAsB,CAAC;AAE3D;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,EACrC,GAAG,EAAE;IAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;CAAE,GACrC,OAAO,CAAC,OAAO,CAAC,CAkClB;AAED,eAAO,MAAM,uBAAuB,EAAG,gBAAyB,CAAC;AAEjE,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,EACrC,GAAG,EAAE;IAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;CAAE,GACrC,OAAO,CAAC,OAAO,CAAC,CA4BlB;AAED,eAAO,MAAM,kBAAkB,EAAG,WAAoB,CAAC;AAEvD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,EACrC,GAAG,EAAE;IAAE,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,IAAI,CAAA;CAAE,GACrC,OAAO,CAAC,OAAO,CAAC,CA6BlB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IACtC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;CACrF,GAAG,IAAI,CAIP"}
@@ -36,9 +36,74 @@ export async function envMigrateJobHandler(job, ctx) {
36
36
  throw err;
37
37
  }
38
38
  }
39
+ export const ENV_FILES_SYNC_JOB_TYPE = 'env.files-sync';
40
+ export async function envFilesSyncJobHandler(job, ctx) {
41
+ let runMigration;
42
+ let resolveFilesConfig;
43
+ try {
44
+ const hazoEnv = await import('hazo_env');
45
+ runMigration = hazoEnv.runMigration;
46
+ resolveFilesConfig = hazoEnv.resolveFilesConfig;
47
+ }
48
+ catch (err) {
49
+ ctx.log('[env_files_sync_handler] hazo_env not installed:', err);
50
+ throw err;
51
+ }
52
+ const progressDir = path.join(resolveFilesConfig().local.basePath, 'migrations', 'progress');
53
+ const raw = job.payload;
54
+ const payload = typeof raw === 'string' ? JSON.parse(raw) : (raw && typeof raw === 'object' ? raw : {});
55
+ try {
56
+ const result = await runMigration({
57
+ from: payload['from'],
58
+ to: payload['to'],
59
+ include: { db: false, files: true },
60
+ jobId: job.id,
61
+ progressDir,
62
+ });
63
+ return result;
64
+ }
65
+ catch (err) {
66
+ ctx.log(`[env_files_sync_handler] job ${job.id} failed:`, err);
67
+ throw err;
68
+ }
69
+ }
70
+ export const ENV_CLEAR_JOB_TYPE = 'env.clear';
71
+ export async function envClearJobHandler(job, ctx) {
72
+ let clearEnv;
73
+ let resolveFilesConfig;
74
+ try {
75
+ const hazoEnv = await import('hazo_env');
76
+ clearEnv = hazoEnv.clearEnv;
77
+ resolveFilesConfig = hazoEnv.resolveFilesConfig;
78
+ }
79
+ catch (err) {
80
+ ctx.log('[env_clear_handler] hazo_env not installed:', err);
81
+ throw err;
82
+ }
83
+ const progressDir = path.join(resolveFilesConfig().local.basePath, 'migrations', 'progress');
84
+ const raw = job.payload;
85
+ const payload = typeof raw === 'string' ? JSON.parse(raw) : (raw && typeof raw === 'object' ? raw : {});
86
+ try {
87
+ const result = await clearEnv({
88
+ env: payload['env'],
89
+ include: payload['include'] ?? { db: true, files: true },
90
+ allowProdTarget: payload['allowProdTarget'],
91
+ confirmToken: payload['confirmToken'],
92
+ jobId: job.id,
93
+ progressDir,
94
+ });
95
+ return result;
96
+ }
97
+ catch (err) {
98
+ ctx.log(`[env_clear_handler] job ${job.id} failed:`, err);
99
+ throw err;
100
+ }
101
+ }
39
102
  /**
40
103
  * Register the env.migrate job type with a hazo_jobs worker.
41
104
  */
42
105
  export function registerEnvJobs(worker) {
43
106
  worker.register(ENV_MIGRATE_JOB_TYPE, envMigrateJobHandler);
107
+ worker.register(ENV_FILES_SYNC_JOB_TYPE, envFilesSyncJobHandler);
108
+ worker.register(ENV_CLEAR_JOB_TYPE, envClearJobHandler);
44
109
  }
@@ -46,5 +46,5 @@ export declare function createAdminPresetRoutes(manifest: AdminManifest, cfg: Ad
46
46
  }>;
47
47
  }) => Promise<Response>;
48
48
  };
49
- export { envMigrateJobHandler, ENV_MIGRATE_JOB_TYPE, registerEnvJobs, } from './env_migrate_handler.js';
49
+ export { envMigrateJobHandler, ENV_MIGRATE_JOB_TYPE, envFilesSyncJobHandler, ENV_FILES_SYNC_JOB_TYPE, envClearJobHandler, ENV_CLEAR_JOB_TYPE, registerEnvJobs, } from './env_migrate_handler.js';
50
50
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAIrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;;OAIG;IACH,cAAc,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE;QACL,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,wFAAwF;QACxF,OAAO,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE;QACJ,gFAAgF;QAChF,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,MAAM,CAAC,EAAE;QACP,2DAA2D;QAC3D,kBAAkB,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;CACH,CAAC;AAuBF,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,EAAE,uBAAuB;mBAudlE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;oBAAzE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;mBAAzE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;sBAAzE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;EAuGnG;AAGD,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,eAAe,GAChB,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAIrB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE,MAAM,MAAM,uBAAuB,GAAG;IACpC;;;;OAIG;IACH,cAAc,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE;QACL,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,wFAAwF;QACxF,OAAO,CAAC,EAAE,IAAI,GAAG,QAAQ,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE;QACJ,gFAAgF;QAChF,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,MAAM,CAAC,EAAE;QACP,2DAA2D;QAC3D,kBAAkB,CAAC,EAAE,MAAM,CAAC;KAC7B,CAAC;CACH,CAAC;AAuBF,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,EAAE,uBAAuB;mBAmiBlE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;oBAAzE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;mBAAzE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;sBAAzE,OAAO,OAAO;QAAE,MAAM,EAAE,OAAO,CAAC;YAAE,IAAI,EAAE,MAAM,EAAE,CAAA;SAAE,CAAC,CAAA;KAAE,KAAG,OAAO,CAAC,QAAQ,CAAC;EAuGnG;AAGD,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,eAAe,GAChB,MAAM,0BAA0B,CAAC"}
package/dist/api/index.js CHANGED
@@ -261,6 +261,78 @@ export function createAdminPresetRoutes(manifest, cfg) {
261
261
  headers: { 'Content-Type': 'application/json' },
262
262
  });
263
263
  }
264
+ // POST /env/files-sync — enqueue a files-only migration job
265
+ if (method === 'POST' && s[0] === 'files-sync' && s.length === 1) {
266
+ const jobsMod = await import('hazo_jobs/server').catch(() => null);
267
+ if (!jobsMod)
268
+ return notInstalled('hazo_jobs');
269
+ const body = await request.json();
270
+ const connect = await cfg.getHazoConnect();
271
+ const jobsClient = jobsMod.createJobsClient({
272
+ connect: { adapter: connect },
273
+ dialect: cfg.jobs?.dialect ?? 'pg',
274
+ });
275
+ const { jobId } = await jobsClient.submit({
276
+ type: 'env.files-sync',
277
+ description: `env-files-sync:${body.from}→${body.to}`,
278
+ payload: body,
279
+ });
280
+ return new Response(JSON.stringify({ jobId }), {
281
+ status: 200,
282
+ headers: { 'Content-Type': 'application/json' },
283
+ });
284
+ }
285
+ // GET /env/files-sync/:jobId
286
+ if (method === 'GET' && s[0] === 'files-sync' && s[1] && s.length === 2) {
287
+ const jobsMod = await import('hazo_jobs/server').catch(() => null);
288
+ if (!jobsMod)
289
+ return notInstalled('hazo_jobs');
290
+ const connect = await cfg.getHazoConnect();
291
+ const jobsClient = jobsMod.createJobsClient({ connect: { adapter: connect }, dialect: cfg.jobs?.dialect ?? 'pg' });
292
+ const job = await jobsClient.get(s[1]);
293
+ return new Response(JSON.stringify(job), { status: 200, headers: { 'Content-Type': 'application/json' } });
294
+ }
295
+ // GET /env/files-sync/:jobId/progress
296
+ if (method === 'GET' && s[0] === 'files-sync' && s[1] && s[2] === 'progress' && s.length === 3) {
297
+ const progress = hazoEnv.readMigrationProgress(progressDir, s[1]);
298
+ return new Response(JSON.stringify(progress), { status: 200, headers: { 'Content-Type': 'application/json' } });
299
+ }
300
+ // POST /env/clear — enqueue a clear job
301
+ if (method === 'POST' && s[0] === 'clear' && s.length === 1) {
302
+ const jobsMod = await import('hazo_jobs/server').catch(() => null);
303
+ if (!jobsMod)
304
+ return notInstalled('hazo_jobs');
305
+ const body = await request.json();
306
+ const connect = await cfg.getHazoConnect();
307
+ const jobsClient = jobsMod.createJobsClient({
308
+ connect: { adapter: connect },
309
+ dialect: cfg.jobs?.dialect ?? 'pg',
310
+ });
311
+ const { jobId } = await jobsClient.submit({
312
+ type: 'env.clear',
313
+ description: `env-clear:${body.env}`,
314
+ payload: body,
315
+ });
316
+ return new Response(JSON.stringify({ jobId }), {
317
+ status: 200,
318
+ headers: { 'Content-Type': 'application/json' },
319
+ });
320
+ }
321
+ // GET /env/clear/:jobId
322
+ if (method === 'GET' && s[0] === 'clear' && s[1] && s.length === 2) {
323
+ const jobsMod = await import('hazo_jobs/server').catch(() => null);
324
+ if (!jobsMod)
325
+ return notInstalled('hazo_jobs');
326
+ const connect = await cfg.getHazoConnect();
327
+ const jobsClient = jobsMod.createJobsClient({ connect: { adapter: connect }, dialect: cfg.jobs?.dialect ?? 'pg' });
328
+ const job = await jobsClient.get(s[1]);
329
+ return new Response(JSON.stringify(job), { status: 200, headers: { 'Content-Type': 'application/json' } });
330
+ }
331
+ // GET /env/clear/:jobId/progress
332
+ if (method === 'GET' && s[0] === 'clear' && s[1] && s[2] === 'progress' && s.length === 3) {
333
+ const progress = hazoEnv.readMigrationProgress(progressDir, s[1]);
334
+ return new Response(JSON.stringify(progress), { status: 200, headers: { 'Content-Type': 'application/json' } });
335
+ }
264
336
  return notFound();
265
337
  }
266
338
  async function handleAudit(_request, _method, _segments) {
@@ -532,4 +604,4 @@ export function createAdminPresetRoutes(manifest, cfg) {
532
604
  };
533
605
  }
534
606
  // Re-export env job handler utilities
535
- export { envMigrateJobHandler, ENV_MIGRATE_JOB_TYPE, registerEnvJobs, } from './env_migrate_handler.js';
607
+ export { envMigrateJobHandler, ENV_MIGRATE_JOB_TYPE, envFilesSyncJobHandler, ENV_FILES_SYNC_JOB_TYPE, envClearJobHandler, ENV_CLEAR_JOB_TYPE, registerEnvJobs, } from './env_migrate_handler.js';
@@ -1 +1 @@
1
- {"version":3,"file":"env_migration_panel.d.ts","sourceRoot":"","sources":["../../src/components/env_migration_panel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAE3D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAkDD,wBAAgB,iBAAiB,CAAC,EAAE,QAAuB,EAAE,EAAE,sBAAsB,qBA+dpF"}
1
+ {"version":3,"file":"env_migration_panel.d.ts","sourceRoot":"","sources":["../../src/components/env_migration_panel.tsx"],"names":[],"mappings":"AACA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAE3D,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAkDD,wBAAgB,iBAAiB,CAAC,EAAE,QAAuB,EAAE,EAAE,sBAAsB,qBAgrBpF"}
@@ -42,6 +42,24 @@ export function EnvMigrationPanel({ basePath = '/api/admin' }) {
42
42
  // Restore state
43
43
  const [restoreMsg, setRestoreMsg] = useState(null);
44
44
  const pollRef = useRef(null);
45
+ // File Sync state
46
+ const [fsFrom, setFsFrom] = useState('');
47
+ const [fsTo, setFsTo] = useState('');
48
+ const [fsRunning, setFsRunning] = useState(false);
49
+ const [fsProgress, setFsProgress] = useState(null);
50
+ const [fsJob, setFsJob] = useState(null);
51
+ const [fsError, setFsError] = useState(null);
52
+ const fsPollRef = useRef(null);
53
+ // Clear state
54
+ const [clrEnv, setClrEnv] = useState('');
55
+ const [clrIncludeDb, setClrIncludeDb] = useState(true);
56
+ const [clrIncludeFiles, setClrIncludeFiles] = useState(true);
57
+ const [clrConfirm, setClrConfirm] = useState('');
58
+ const [clrRunning, setClrRunning] = useState(false);
59
+ const [clrProgress, setClrProgress] = useState(null);
60
+ const [clrJob, setClrJob] = useState(null);
61
+ const [clrError, setClrError] = useState(null);
62
+ const clrPollRef = useRef(null);
45
63
  // Load env list on mount
46
64
  useEffect(() => {
47
65
  fetch(`${basePath}/env/list`, { credentials: 'include' })
@@ -54,6 +72,9 @@ export function EnvMigrationPanel({ basePath = '/api/admin' }) {
54
72
  setFrom(data[0].name);
55
73
  if (data.length > 1)
56
74
  setTo(data[1].name);
75
+ setFsFrom(data[0].name);
76
+ setFsTo(data.length > 1 ? data[1].name : data[0].name);
77
+ setClrEnv(data.length > 1 ? data[1].name : data[0].name);
57
78
  }
58
79
  else {
59
80
  setEnvsError('Failed to parse env list');
@@ -80,6 +101,10 @@ export function EnvMigrationPanel({ basePath = '/api/admin' }) {
80
101
  return () => {
81
102
  if (pollRef.current !== null)
82
103
  clearInterval(pollRef.current);
104
+ if (fsPollRef.current !== null)
105
+ clearInterval(fsPollRef.current);
106
+ if (clrPollRef.current !== null)
107
+ clearInterval(clrPollRef.current);
83
108
  };
84
109
  }, []);
85
110
  function stopPolling() {
@@ -182,6 +207,115 @@ export function EnvMigrationPanel({ basePath = '/api/admin' }) {
182
207
  const isProdTarget = envs.find(e => e.name === to)?.role === 'production';
183
208
  const prodConfirmValid = !isProdTarget || prodConfirm === to;
184
209
  const canRun = !running && prodConfirmValid && !!from && !!to && from !== to;
210
+ function startFsPolling(id) {
211
+ if (fsPollRef.current !== null)
212
+ clearInterval(fsPollRef.current);
213
+ fsPollRef.current = setInterval(async () => {
214
+ try {
215
+ const [progressRes, jobRes] = await Promise.all([
216
+ fetch(`${basePath}/env/files-sync/${id}/progress`, { credentials: 'include' }),
217
+ fetch(`${basePath}/env/files-sync/${id}`, { credentials: 'include' }),
218
+ ]);
219
+ if (!progressRes.ok || !jobRes.ok)
220
+ return;
221
+ const progressData = await progressRes.json();
222
+ const jobData = await jobRes.json();
223
+ setFsProgress(progressData);
224
+ setFsJob(jobData);
225
+ if (jobData.status === 'completed' || jobData.status === 'failed') {
226
+ clearInterval(fsPollRef.current);
227
+ fsPollRef.current = null;
228
+ setFsRunning(false);
229
+ }
230
+ }
231
+ catch { /* non-fatal */ }
232
+ }, 1500);
233
+ }
234
+ async function handleFsSubmit(e) {
235
+ e.preventDefault();
236
+ setFsError(null);
237
+ setFsProgress(null);
238
+ setFsJob(null);
239
+ setFsRunning(true);
240
+ try {
241
+ const res = await fetch(`${basePath}/env/files-sync`, {
242
+ method: 'POST', credentials: 'include',
243
+ headers: { 'Content-Type': 'application/json' },
244
+ body: JSON.stringify({ from: fsFrom, to: fsTo }),
245
+ });
246
+ const data = await res.json();
247
+ if (!res.ok || !data.jobId) {
248
+ setFsError(typeof data.error === 'string' ? data.error : 'File sync failed to start');
249
+ setFsRunning(false);
250
+ return;
251
+ }
252
+ startFsPolling(data.jobId);
253
+ }
254
+ catch (err) {
255
+ setFsError(err instanceof Error ? err.message : 'Network error');
256
+ setFsRunning(false);
257
+ }
258
+ }
259
+ function startClrPolling(id) {
260
+ if (clrPollRef.current !== null)
261
+ clearInterval(clrPollRef.current);
262
+ clrPollRef.current = setInterval(async () => {
263
+ try {
264
+ const [progressRes, jobRes] = await Promise.all([
265
+ fetch(`${basePath}/env/clear/${id}/progress`, { credentials: 'include' }),
266
+ fetch(`${basePath}/env/clear/${id}`, { credentials: 'include' }),
267
+ ]);
268
+ if (!progressRes.ok || !jobRes.ok)
269
+ return;
270
+ const progressData = await progressRes.json();
271
+ const jobData = await jobRes.json();
272
+ setClrProgress(progressData);
273
+ setClrJob(jobData);
274
+ if (jobData.status === 'completed' || jobData.status === 'failed') {
275
+ clearInterval(clrPollRef.current);
276
+ clrPollRef.current = null;
277
+ setClrRunning(false);
278
+ }
279
+ }
280
+ catch { /* non-fatal */ }
281
+ }, 1500);
282
+ }
283
+ async function handleClrSubmit(e) {
284
+ e.preventDefault();
285
+ setClrError(null);
286
+ setClrProgress(null);
287
+ setClrJob(null);
288
+ const isProd = envs.find(e => e.name === clrEnv)?.role === 'production';
289
+ if (isProd && clrConfirm !== clrEnv)
290
+ return;
291
+ setClrRunning(true);
292
+ const payload = {
293
+ env: clrEnv,
294
+ include: { db: clrIncludeDb, files: clrIncludeFiles },
295
+ };
296
+ if (isProd) {
297
+ payload['allowProdTarget'] = true;
298
+ payload['confirmToken'] = clrConfirm;
299
+ }
300
+ try {
301
+ const res = await fetch(`${basePath}/env/clear`, {
302
+ method: 'POST', credentials: 'include',
303
+ headers: { 'Content-Type': 'application/json' },
304
+ body: JSON.stringify(payload),
305
+ });
306
+ const data = await res.json();
307
+ if (!res.ok || !data.jobId) {
308
+ setClrError(typeof data.error === 'string' ? data.error : 'Clear failed to start');
309
+ setClrRunning(false);
310
+ return;
311
+ }
312
+ startClrPolling(data.jobId);
313
+ }
314
+ catch (err) {
315
+ setClrError(err instanceof Error ? err.message : 'Network error');
316
+ setClrRunning(false);
317
+ }
318
+ }
185
319
  return (_jsxs("div", { className: "p-6 max-w-2xl space-y-6", children: [_jsx("h2", { className: "text-base font-semibold text-gray-800", children: "Env Migration" }), envsError && (_jsx("div", { className: "text-sm text-red-500", children: envsError })), _jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [_jsxs("div", { className: "flex gap-4", children: [_jsxs("div", { className: "flex-1", children: [_jsx("label", { className: "block text-xs font-medium text-gray-600 mb-1", children: "From" }), _jsx("select", { value: from, onChange: e => setFrom(e.target.value), className: "w-full border border-gray-300 rounded px-2 py-1.5 text-sm", disabled: running, children: envs.map(env => (_jsxs("option", { value: env.name, children: [env.name, " ", env.role !== 'production' ? `(${env.role})` : '(PRODUCTION)'] }, env.name))) })] }), _jsxs("div", { className: "flex-1", children: [_jsx("label", { className: "block text-xs font-medium text-gray-600 mb-1", children: "To" }), _jsx("select", { value: to, onChange: e => { setTo(e.target.value); setProdConfirm(''); }, className: "w-full border border-gray-300 rounded px-2 py-1.5 text-sm", disabled: running, children: envs.map(env => (_jsxs("option", { value: env.name, children: [env.name, " ", env.role !== 'production' ? `(${env.role})` : '(PRODUCTION)'] }, env.name))) })] })] }), _jsxs("div", { className: "flex gap-6", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm text-gray-700", children: [_jsx("input", { type: "checkbox", checked: includeDb, onChange: e => setIncludeDb(e.target.checked), disabled: running }), "Include DB"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm text-gray-700", children: [_jsx("input", { type: "checkbox", checked: includeFiles, onChange: e => setIncludeFiles(e.target.checked), disabled: running }), "Include Files"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm text-gray-700", children: [_jsx("input", { type: "checkbox", checked: dryRun, onChange: e => setDryRun(e.target.checked), disabled: running }), "Dry run"] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-medium text-gray-600 mb-1", children: "Transport" }), _jsxs("select", { value: transport, onChange: e => setTransport(e.target.value), className: "border border-gray-300 rounded px-2 py-1.5 text-sm", disabled: running, children: [_jsx("option", { value: "auto", children: "auto" }), _jsx("option", { value: "local", children: "local" }), _jsx("option", { value: "api", children: "api" }), _jsx("option", { value: "ssh", disabled: true, children: "ssh (coming soon)" })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-medium text-gray-600 mb-1", children: "Scrub (PII masking)" }), _jsxs("select", { value: scrub, onChange: e => setScrub(e.target.value), className: "border border-gray-300 rounded px-2 py-1.5 text-sm", disabled: running, children: [_jsx("option", { value: "none", children: "none" }), _jsx("option", { value: "auto", children: "auto" })] })] }), _jsxs("div", { children: [_jsx("label", { className: "block text-xs font-medium text-gray-600 mb-1", children: "Tables (glob, * = all)" }), _jsx("input", { type: "text", value: tables, onChange: e => setTables(e.target.value), className: "w-full border border-gray-300 rounded px-2 py-1.5 text-sm", disabled: running })] }), isProdTarget && (_jsxs("div", { className: "border border-red-300 rounded p-3 space-y-2 bg-red-50", children: [_jsx("div", { className: "text-sm font-medium text-red-700", children: "Warning: you are targeting a production environment." }), _jsxs("label", { className: "block text-xs font-medium text-red-600 mb-1", children: ["Type ", _jsx("code", { className: "font-mono bg-red-100 px-1 rounded", children: to }), " to confirm"] }), _jsx("input", { type: "text", value: prodConfirm, onChange: e => setProdConfirm(e.target.value), placeholder: to, className: "w-full border border-red-300 rounded px-2 py-1.5 text-sm", disabled: running })] })), _jsx("div", { children: _jsx("button", { type: "submit", disabled: !canRun, className: "px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed", children: running ? 'Running…' : dryRun ? 'Run dry run' : 'Run migration' }) })] }), running && progress && (_jsxs("div", { className: "space-y-2", children: [_jsx("div", { className: "text-xs text-gray-500 uppercase tracking-wide font-medium", children: "Progress" }), _jsxs("div", { className: "text-sm text-gray-700", children: [progress.phase, ": ", progress.message] }), _jsx("div", { className: "w-full bg-gray-200 rounded h-2", children: _jsx("div", { className: "bg-blue-500 h-2 rounded transition-all", style: { width: `${Math.max(0, Math.min(100, progress.percent))}%` } }) }), _jsxs("div", { className: "text-xs text-gray-500", children: [progress.percent, "%"] })] })), submitError && (_jsx("div", { className: "text-sm text-red-600 border border-red-200 rounded p-3 bg-red-50", children: submitError })), job?.status === 'completed' && (() => {
186
320
  const result = normalizeResult(job.result);
187
321
  const db = result?.db;
@@ -212,5 +346,5 @@ export function EnvMigrationPanel({ basePath = '/api/admin' }) {
212
346
  })(), job?.status === 'failed' && (() => {
213
347
  const result = normalizeResult(job.result);
214
348
  return (_jsxs("div", { className: "border border-red-200 rounded p-4 space-y-3 bg-red-50", children: [_jsx("div", { className: "text-sm font-medium text-red-700", children: "Migration failed" }), job.error && _jsx("div", { className: "text-sm text-red-600", children: job.error }), result?.warnings && result.warnings.length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs font-medium text-red-600", children: "Warnings" }), result.warnings.map((w, i) => (_jsx("div", { className: "text-xs text-red-600", children: w }, i)))] })), result?.snapshotId && (_jsx("button", { onClick: () => handleRestore(result.snapshotId), disabled: running, className: "px-3 py-1.5 bg-red-600 text-white text-xs font-medium rounded hover:bg-red-700 disabled:opacity-50", children: "Restore last snapshot" }))] }));
215
- })(), restoreMsg && (_jsx("div", { className: `text-sm border rounded p-3 ${restoreMsg.startsWith('Snapshot restored') ? 'border-green-200 bg-green-50 text-green-700' : 'border-red-200 bg-red-50 text-red-600'}`, children: restoreMsg })), to && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-xs text-gray-500 uppercase tracking-wide font-medium", children: ["Snapshots for \"", to, "\""] }), snapshotsLoading && _jsx("div", { className: "text-xs text-gray-400", children: "Loading snapshots\u2026" }), !snapshotsLoading && snapshots.length === 0 && (_jsx("div", { className: "text-xs text-gray-400", children: "No snapshots found." })), !snapshotsLoading && snapshots.length > 0 && (_jsx("ul", { className: "space-y-1", children: snapshots.map(snap => (_jsxs("li", { className: "flex items-center justify-between text-xs border rounded px-3 py-2", children: [_jsxs("div", { children: [_jsx("div", { className: "font-mono text-gray-700", children: snap.fileName }), _jsx("div", { className: "text-gray-400", children: snap.createdAt })] }), _jsx("button", { onClick: () => handleRestore(snap.snapshotId), disabled: running, className: "px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed", children: "Restore" })] }, snap.snapshotId))) }))] }))] }));
349
+ })(), restoreMsg && (_jsx("div", { className: `text-sm border rounded p-3 ${restoreMsg.startsWith('Snapshot restored') ? 'border-green-200 bg-green-50 text-green-700' : 'border-red-200 bg-red-50 text-red-600'}`, children: restoreMsg })), to && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-xs text-gray-500 uppercase tracking-wide font-medium", children: ["Snapshots for \"", to, "\""] }), snapshotsLoading && _jsx("div", { className: "text-xs text-gray-400", children: "Loading snapshots\u2026" }), !snapshotsLoading && snapshots.length === 0 && (_jsx("div", { className: "text-xs text-gray-400", children: "No snapshots found." })), !snapshotsLoading && snapshots.length > 0 && (_jsx("ul", { className: "space-y-1", children: snapshots.map(snap => (_jsxs("li", { className: "flex items-center justify-between text-xs border rounded px-3 py-2", children: [_jsxs("div", { children: [_jsx("div", { className: "font-mono text-gray-700", children: snap.fileName }), _jsx("div", { className: "text-gray-400", children: snap.createdAt })] }), _jsx("button", { onClick: () => handleRestore(snap.snapshotId), disabled: running, className: "px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 border border-gray-300 rounded disabled:opacity-50 disabled:cursor-not-allowed", children: "Restore" })] }, snap.snapshotId))) }))] })), _jsxs("div", { className: "border-t border-gray-200 pt-6 space-y-4", children: [_jsx("h3", { className: "text-sm font-semibold text-gray-700", children: "File Sync" }), _jsx("p", { className: "text-xs text-gray-500", children: "Copy files only (no DB) between environments via rsync." }), _jsxs("form", { onSubmit: handleFsSubmit, className: "space-y-4", children: [_jsxs("div", { className: "flex gap-4", children: [_jsxs("div", { className: "flex-1", children: [_jsx("label", { className: "block text-xs font-medium text-gray-600 mb-1", children: "From" }), _jsx("select", { value: fsFrom, onChange: e => setFsFrom(e.target.value), className: "w-full border border-gray-300 rounded px-2 py-1.5 text-sm", disabled: fsRunning, children: envs.map(env => _jsx("option", { value: env.name, children: env.name }, env.name)) })] }), _jsxs("div", { className: "flex-1", children: [_jsx("label", { className: "block text-xs font-medium text-gray-600 mb-1", children: "To" }), _jsx("select", { value: fsTo, onChange: e => setFsTo(e.target.value), className: "w-full border border-gray-300 rounded px-2 py-1.5 text-sm", disabled: fsRunning, children: envs.map(env => _jsx("option", { value: env.name, children: env.name }, env.name)) })] })] }), _jsx("button", { type: "submit", disabled: fsRunning || !fsFrom || !fsTo || fsFrom === fsTo, className: "px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed", children: fsRunning ? 'Syncing…' : 'Sync files' })] }), fsRunning && fsProgress && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm text-gray-700", children: [fsProgress.phase, ": ", fsProgress.message] }), _jsx("div", { className: "w-full bg-gray-200 rounded h-2", children: _jsx("div", { className: "bg-blue-500 h-2 rounded transition-all", style: { width: `${Math.max(0, Math.min(100, fsProgress.percent))}%` } }) }), _jsxs("div", { className: "text-xs text-gray-500", children: [fsProgress.percent, "%"] })] })), fsError && _jsx("div", { className: "text-sm text-red-600 border border-red-200 rounded p-3 bg-red-50", children: fsError }), fsJob?.status === 'completed' && _jsx("div", { className: "text-sm text-green-700 border border-green-200 rounded p-3 bg-green-50", children: "File sync complete." }), fsJob?.status === 'failed' && _jsxs("div", { className: "text-sm text-red-600 border border-red-200 rounded p-3 bg-red-50", children: ["File sync failed. ", fsJob.error] })] }), _jsxs("div", { className: "border-t border-gray-200 pt-6 space-y-4", children: [_jsx("h3", { className: "text-sm font-semibold text-red-700", children: "Clear Environment" }), _jsx("p", { className: "text-xs text-gray-500", children: "Delete all non-preserved rows from the target DB and/or wipe its file directory. Irreversible." }), _jsxs("form", { onSubmit: handleClrSubmit, className: "space-y-4", children: [_jsxs("div", { children: [_jsx("label", { className: "block text-xs font-medium text-gray-600 mb-1", children: "Target" }), _jsx("select", { value: clrEnv, onChange: e => { setClrEnv(e.target.value); setClrConfirm(''); }, className: "w-full border border-gray-300 rounded px-2 py-1.5 text-sm", disabled: clrRunning, children: envs.map(env => _jsxs("option", { value: env.name, children: [env.name, " ", env.role === 'production' ? '(PRODUCTION)' : `(${env.role})`] }, env.name)) })] }), _jsxs("div", { className: "flex gap-6", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm text-gray-700", children: [_jsx("input", { type: "checkbox", checked: clrIncludeDb, onChange: e => setClrIncludeDb(e.target.checked), disabled: clrRunning }), "Clear DB"] }), _jsxs("label", { className: "flex items-center gap-2 text-sm text-gray-700", children: [_jsx("input", { type: "checkbox", checked: clrIncludeFiles, onChange: e => setClrIncludeFiles(e.target.checked), disabled: clrRunning }), "Clear Files"] })] }), envs.find(e => e.name === clrEnv)?.role === 'production' && (_jsxs("div", { className: "border border-red-300 rounded p-3 space-y-2 bg-red-50", children: [_jsx("div", { className: "text-sm font-medium text-red-700", children: "Danger: clearing a production environment." }), _jsxs("label", { className: "block text-xs font-medium text-red-600 mb-1", children: ["Type ", _jsx("code", { className: "font-mono bg-red-100 px-1 rounded", children: clrEnv }), " to confirm"] }), _jsx("input", { type: "text", value: clrConfirm, onChange: e => setClrConfirm(e.target.value), placeholder: clrEnv, className: "w-full border border-red-300 rounded px-2 py-1.5 text-sm", disabled: clrRunning })] })), _jsx("button", { type: "submit", disabled: clrRunning || !clrEnv || (!clrIncludeDb && !clrIncludeFiles) || (envs.find(e => e.name === clrEnv)?.role === 'production' && clrConfirm !== clrEnv), className: "px-4 py-2 bg-red-600 text-white text-sm font-medium rounded hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed", children: clrRunning ? 'Clearing…' : 'Clear environment' })] }), clrRunning && clrProgress && (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "text-sm text-gray-700", children: [clrProgress.phase, ": ", clrProgress.message] }), _jsx("div", { className: "w-full bg-gray-200 rounded h-2", children: _jsx("div", { className: "bg-red-500 h-2 rounded transition-all", style: { width: `${Math.max(0, Math.min(100, clrProgress.percent))}%` } }) }), _jsxs("div", { className: "text-xs text-gray-500", children: [clrProgress.percent, "%"] })] })), clrError && _jsx("div", { className: "text-sm text-red-600 border border-red-200 rounded p-3 bg-red-50", children: clrError }), clrJob?.status === 'completed' && _jsx("div", { className: "text-sm text-green-700 border border-green-200 rounded p-3 bg-green-50", children: "Environment cleared." }), clrJob?.status === 'failed' && _jsxs("div", { className: "text-sm text-red-600 border border-red-200 rounded p-3 bg-red-50", children: ["Clear failed. ", clrJob.error] })] })] }));
216
350
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_admin",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "description": "Standard site-admin package — auth-gated admin shell + panel kit + drop-in /admin preset",
5
5
  "type": "module",
6
6
  "module": "./dist/index.js",