pre-migration prepped
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
VITE_TASGRID_ENABLE_PROD_MIGRATION=true
|
||||
VITE_TASGRID_MIGRATION_ADMIN_EMAILS=tcardoza@cardoza.construction
|
||||
@@ -2,10 +2,17 @@ const rawMode = (import.meta.env.VITE_TASGRID_DATA_MODE || "prod").toLowerCase()
|
||||
|
||||
export const TASGRID_DATA_MODE = rawMode === "dev" ? "dev" : "prod";
|
||||
export const TASGRID_IS_DEV_DATA = TASGRID_DATA_MODE === "dev";
|
||||
const rawMigrationAdmins = import.meta.env.VITE_TASGRID_MIGRATION_ADMIN_EMAILS || "";
|
||||
const rawMigrationEnabled = (import.meta.env.VITE_TASGRID_ENABLE_PROD_MIGRATION || "").toLowerCase();
|
||||
|
||||
export const POCKETBASE_URL = import.meta.env.VITE_POCKETBASE_URL || "https://pocketbase.ccllc.pro";
|
||||
export const AUTH_STORE_KEY = TASGRID_IS_DEV_DATA ? "tasgrid_dev_auth" : "tasgrid_auth";
|
||||
export const STORAGE_KEY_PREFIX = TASGRID_IS_DEV_DATA ? "tasgrid_dev" : "tasgrid";
|
||||
export const PWA_CACHE_SUFFIX = TASGRID_IS_DEV_DATA ? "dev" : "prod";
|
||||
export const PROD_MIGRATION_ENABLED = rawMigrationEnabled === "true";
|
||||
export const PROD_MIGRATION_ADMIN_EMAILS = rawMigrationAdmins
|
||||
.split(",")
|
||||
.map((email: string) => email.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
|
||||
export const withDataSuffix = (base: string) => TASGRID_IS_DEV_DATA ? `${base}_Dev` : base;
|
||||
|
||||
+204
-56
@@ -3,7 +3,7 @@ import { createSignal, createEffect, createRoot } from "solid-js";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { toast } from "solid-sonner";
|
||||
import { CONTEXTS_COLLECTION, NOTES_COLLECTION, TASGRID_COLLECTION, URGENCY_HOURS, SIZE_HOURS } from "@/lib/constants";
|
||||
import { STORAGE_KEY_PREFIX, TASGRID_DATA_MODE } from "@/lib/app-config";
|
||||
import { STORAGE_KEY_PREFIX, TASGRID_DATA_MODE, TASGRID_IS_DEV_DATA, PROD_MIGRATION_ADMIN_EMAILS, PROD_MIGRATION_ENABLED } from "@/lib/app-config";
|
||||
import { buildNoteTag, buildShareTag, normalizeContextKey, normalizeNoteKey, parseTags } from "@/lib/tags";
|
||||
|
||||
// Helper to get the current user's personalized favorite tag
|
||||
@@ -536,6 +536,26 @@ const buildTaskTags = (
|
||||
return [...new Set(tags.filter(Boolean))];
|
||||
};
|
||||
|
||||
const dedupeShareRefs = (refs: TaskShareRef[]) => {
|
||||
const seen = new Set<string>();
|
||||
return refs.filter(ref => {
|
||||
const key = `${ref.kind}:${ref.contextId}:${ref.key}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const dedupeNoteRefs = (refs: NoteRef[]) => {
|
||||
const seen = new Set<string>();
|
||||
return refs.filter(ref => {
|
||||
const key = `${ref.noteId}:${ref.key}`;
|
||||
if (seen.has(key)) return false;
|
||||
seen.add(key);
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
const withActiveContextTags = (rawTags: string[]) => {
|
||||
const nextTags = [...rawTags];
|
||||
const ctx = currentTaskContext();
|
||||
@@ -586,6 +606,17 @@ const setContexts = (contexts: ShareContext[]) => {
|
||||
setStore("tagDefinitions", reconcile(mergedDefs));
|
||||
};
|
||||
|
||||
const loadContextsIntoStore = async () => {
|
||||
const contextRecords = await pb.collection(CONTEXTS_COLLECTION).getFullList({
|
||||
sort: 'displayName,key',
|
||||
requestKey: null
|
||||
}).catch(() => []);
|
||||
const nextContexts = contextRecords.map(mapRecordToContext);
|
||||
setContexts(nextContexts);
|
||||
setStore("shareRules", nextContexts.map(mapContextToShareRule));
|
||||
return nextContexts;
|
||||
};
|
||||
|
||||
const getPersonalContext = (userId = pb.authStore.model?.id) =>
|
||||
userId
|
||||
? store.contexts.find(context =>
|
||||
@@ -631,7 +662,11 @@ export const matchesFilter = (task: Task) => {
|
||||
);
|
||||
const inPersonalContext = !!personalContext && task.shareRefs.some(ref =>
|
||||
ref.kind === "user" &&
|
||||
(ref.contextId === personalContext.id || ref.key === personalContext.key)
|
||||
(
|
||||
ref.contextId === personalContext.id ||
|
||||
ref.key === personalContext.key ||
|
||||
normalizeContextKey(personalContext.displayName) === ref.key
|
||||
)
|
||||
);
|
||||
|
||||
// 0. Context Filter
|
||||
@@ -905,6 +940,66 @@ const mapRecordToTask = (r: any): Task => {
|
||||
};
|
||||
};
|
||||
|
||||
const buildNoteLinkMap = (notes: Note[]) => {
|
||||
const links = new Map<string, NoteRef[]>();
|
||||
for (const note of notes) {
|
||||
for (const taskId of note.tasks || []) {
|
||||
const existing = links.get(taskId) || [];
|
||||
existing.push({ noteId: note.id, key: note.key });
|
||||
links.set(taskId, existing);
|
||||
}
|
||||
}
|
||||
return links;
|
||||
};
|
||||
|
||||
const buildTaskMigrationPayload = (
|
||||
task: Task,
|
||||
ownerContextByUserId: Map<string, ShareContext>,
|
||||
linkedNoteRefs: NoteRef[] = [],
|
||||
fallbackUserId?: string
|
||||
) => {
|
||||
const favoriteTag = task.tags.find(tag => tag.endsWith("_favorite__"));
|
||||
const ownerContext = task.ownerId ? ownerContextByUserId.get(task.ownerId) : undefined;
|
||||
let nextShareRefs = dedupeShareRefs(task.shareRefs);
|
||||
let nextNoteRefs = dedupeNoteRefs([...task.noteRefs, ...linkedNoteRefs]);
|
||||
const hasHandoffBucket = nextShareRefs.some(ref => {
|
||||
const context = getContextByRef(ref);
|
||||
return context?.kind === "bucket" && context.policy === "handoff";
|
||||
});
|
||||
|
||||
if (ownerContext) {
|
||||
const matchesOwnerContext = (ref: TaskShareRef) =>
|
||||
ref.kind === "user" && (
|
||||
ref.contextId === ownerContext.id ||
|
||||
ref.key === ownerContext.key ||
|
||||
normalizeContextKey(ownerContext.displayName) === ref.key
|
||||
);
|
||||
|
||||
if (hasHandoffBucket) {
|
||||
nextShareRefs = nextShareRefs.filter(ref => !matchesOwnerContext(ref));
|
||||
} else if (!nextShareRefs.some(matchesOwnerContext)) {
|
||||
nextShareRefs = [
|
||||
{
|
||||
contextId: ownerContext.id,
|
||||
key: ownerContext.key,
|
||||
kind: "user" as const
|
||||
},
|
||||
...nextShareRefs
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
nextShareRefs = dedupeShareRefs(nextShareRefs);
|
||||
|
||||
return {
|
||||
tags: buildTaskTags(task.labelTags, nextShareRefs, nextNoteRefs, favoriteTag),
|
||||
labelTags: task.labelTags,
|
||||
shareRefs: nextShareRefs,
|
||||
noteRefs: nextNoteRefs,
|
||||
createdBy: task.createdBy || task.ownerId || fallbackUserId || pb.authStore.model?.id || ""
|
||||
};
|
||||
};
|
||||
|
||||
export const uploadTaskAttachment = async (taskId: string, file: File): Promise<{ url: string, filename: string }> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
@@ -1247,7 +1342,7 @@ export const subscribeToRealtime = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
const runLegacyTagMigration = async () => {
|
||||
const runLegacyTagMigration = async (force = false) => {
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
if (!currentUserId) return;
|
||||
|
||||
@@ -1256,13 +1351,45 @@ const runLegacyTagMigration = async () => {
|
||||
const rawPrefs = userRec.Taskgrid_pref || {};
|
||||
const prefs = readScopedPrefs(rawPrefs);
|
||||
|
||||
if (prefs.migratedStructuredTagsV2) {
|
||||
if (!force && prefs.migratedStructuredTagsV2) {
|
||||
return; // Already migrated
|
||||
}
|
||||
|
||||
console.log("[MIGRATION] Starting structured tag migration...");
|
||||
|
||||
await syncUserContexts();
|
||||
const contexts = await loadContextsIntoStore();
|
||||
const ownerContextByUserId = new Map(
|
||||
contexts
|
||||
.filter(context => context.kind === "user" && !!context.targetUserId && !context.deletedAt)
|
||||
.map(context => [context.targetUserId as string, context])
|
||||
);
|
||||
|
||||
const notes = await pb.collection(NOTES_COLLECTION).getFullList({
|
||||
requestKey: null
|
||||
});
|
||||
const migratedNotes: Note[] = [];
|
||||
for (const noteRecord of notes) {
|
||||
const key = normalizeNoteKey(noteRecord.key || noteRecord.title || noteRecord.id);
|
||||
const existingTags = Array.isArray(noteRecord.tags) ? noteRecord.tags.filter(Boolean) : [];
|
||||
const nextTags = [...new Set([buildNoteTag(key), ...existingTags])];
|
||||
const nextTasks = [...new Set((Array.isArray(noteRecord.tasks) ? noteRecord.tasks : []).filter(Boolean))];
|
||||
|
||||
await pb.collection(NOTES_COLLECTION).update(noteRecord.id, {
|
||||
key,
|
||||
tags: nextTags,
|
||||
tasks: nextTasks
|
||||
}, { requestKey: null });
|
||||
|
||||
migratedNotes.push({
|
||||
...mapRecordToNote(noteRecord),
|
||||
key,
|
||||
tags: nextTags,
|
||||
tasks: nextTasks
|
||||
});
|
||||
}
|
||||
setStore("notes", migratedNotes);
|
||||
const noteLinksByTaskId = buildNoteLinkMap(migratedNotes);
|
||||
|
||||
const tasks = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||
requestKey: null
|
||||
@@ -1270,24 +1397,13 @@ const runLegacyTagMigration = async () => {
|
||||
for (const taskRecord of tasks) {
|
||||
if (taskRecord.tags?.includes("__template__")) continue;
|
||||
const task = mapRecordToTask(taskRecord);
|
||||
await pb.collection(TASGRID_COLLECTION).update(task.id, {
|
||||
tags: task.tags,
|
||||
labelTags: task.labelTags,
|
||||
shareRefs: task.shareRefs,
|
||||
noteRefs: task.noteRefs,
|
||||
createdBy: task.createdBy || task.ownerId || currentUserId
|
||||
}, { requestKey: null });
|
||||
}
|
||||
|
||||
const notes = await pb.collection(NOTES_COLLECTION).getFullList({
|
||||
requestKey: null
|
||||
});
|
||||
for (const noteRecord of notes) {
|
||||
const note = mapRecordToNote(noteRecord);
|
||||
await pb.collection(NOTES_COLLECTION).update(note.id, {
|
||||
key: note.key,
|
||||
tags: note.tags
|
||||
}, { requestKey: null });
|
||||
const payload = buildTaskMigrationPayload(
|
||||
task,
|
||||
ownerContextByUserId,
|
||||
noteLinksByTaskId.get(task.id) || [],
|
||||
currentUserId
|
||||
);
|
||||
await pb.collection(TASGRID_COLLECTION).update(task.id, payload, { requestKey: null });
|
||||
}
|
||||
|
||||
const nextPrefs = { ...prefs, migratedStructuredTagsV2: true };
|
||||
@@ -1331,13 +1447,7 @@ const syncUserContexts = async () => {
|
||||
}
|
||||
|
||||
if (createdAny || store.contexts.length === 0) {
|
||||
const nextContextRecords = await pb.collection(CONTEXTS_COLLECTION).getFullList({
|
||||
sort: 'displayName,key',
|
||||
requestKey: null
|
||||
}).catch(() => []);
|
||||
const nextContexts = nextContextRecords.map(mapRecordToContext);
|
||||
setContexts(nextContexts);
|
||||
setStore("shareRules", nextContexts.map(mapContextToShareRule));
|
||||
await loadContextsIntoStore();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1382,7 +1492,7 @@ const syncSupervisedContexts = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const runPersonalContextMigration = async () => {
|
||||
const runPersonalContextMigration = async (force = false) => {
|
||||
const currentUserId = pb.authStore.model?.id;
|
||||
if (!currentUserId) return;
|
||||
|
||||
@@ -1391,18 +1501,19 @@ const runPersonalContextMigration = async () => {
|
||||
const rawPrefs = userRec.Taskgrid_pref || {};
|
||||
const prefs = readScopedPrefs(rawPrefs);
|
||||
|
||||
if (prefs.migratedPersonalContextRefsV1) {
|
||||
if (!force && prefs.migratedPersonalContextRefsV1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contexts = store.contexts.length > 0
|
||||
? store.contexts
|
||||
: (await pb.collection(CONTEXTS_COLLECTION).getFullList({ requestKey: null })).map(mapRecordToContext);
|
||||
: await loadContextsIntoStore();
|
||||
const ownerContextByUserId = new Map(
|
||||
contexts
|
||||
.filter(context => context.kind === "user" && !!context.targetUserId && !context.deletedAt)
|
||||
.map(context => [context.targetUserId as string, context])
|
||||
);
|
||||
const noteLinksByTaskId = buildNoteLinkMap(store.notes);
|
||||
|
||||
const taskRecords = await pb.collection(TASGRID_COLLECTION).getFullList({
|
||||
fields: LIST_FIELDS,
|
||||
@@ -1414,30 +1525,19 @@ const runPersonalContextMigration = async () => {
|
||||
|
||||
const task = mapRecordToTask(taskRecord);
|
||||
if (!task.ownerId) continue;
|
||||
|
||||
const ownerContext = ownerContextByUserId.get(task.ownerId);
|
||||
if (!ownerContext) continue;
|
||||
|
||||
const alreadyHasOwnerContext = task.shareRefs.some(ref =>
|
||||
ref.kind === "user" &&
|
||||
(ref.contextId === ownerContext.id || ref.key === ownerContext.key)
|
||||
const payload = buildTaskMigrationPayload(
|
||||
task,
|
||||
ownerContextByUserId,
|
||||
noteLinksByTaskId.get(task.id) || [],
|
||||
currentUserId
|
||||
);
|
||||
if (alreadyHasOwnerContext) continue;
|
||||
|
||||
const favoriteTag = task.tags.find(tag => tag.endsWith("_favorite__"));
|
||||
const nextShareRefs = [
|
||||
{
|
||||
contextId: ownerContext.id,
|
||||
key: ownerContext.key,
|
||||
kind: "user" as const
|
||||
},
|
||||
...task.shareRefs
|
||||
];
|
||||
const sameShareRefs = JSON.stringify(payload.shareRefs) === JSON.stringify(task.shareRefs);
|
||||
const sameNoteRefs = JSON.stringify(payload.noteRefs) === JSON.stringify(task.noteRefs);
|
||||
const sameTags = JSON.stringify(payload.tags) === JSON.stringify(task.tags);
|
||||
if (sameShareRefs && sameNoteRefs && sameTags && payload.createdBy === task.createdBy) continue;
|
||||
|
||||
await pb.collection(TASGRID_COLLECTION).update(task.id, {
|
||||
shareRefs: nextShareRefs,
|
||||
tags: buildTaskTags(task.labelTags, nextShareRefs, task.noteRefs, favoriteTag)
|
||||
}, { requestKey: null });
|
||||
await pb.collection(TASGRID_COLLECTION).update(task.id, payload, { requestKey: null });
|
||||
}
|
||||
|
||||
await pb.collection('users').update(currentUserId, {
|
||||
@@ -1451,6 +1551,52 @@ const runPersonalContextMigration = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const runDevDataMigration = async () => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
if (!TASGRID_IS_DEV_DATA) {
|
||||
toast.error("Dev migration is only available in dev data mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
await toast.promise((async () => {
|
||||
await runLegacyTagMigration(true);
|
||||
await runPersonalContextMigration(true);
|
||||
await loadContextsIntoStore();
|
||||
await syncSupervisedContexts();
|
||||
return "Dev data migration completed.";
|
||||
})(), {
|
||||
loading: "Running dev data migration...",
|
||||
success: (message) => message,
|
||||
error: "Dev data migration failed."
|
||||
});
|
||||
};
|
||||
|
||||
export const canRunProdMigration = () => {
|
||||
if (TASGRID_IS_DEV_DATA || !PROD_MIGRATION_ENABLED) return false;
|
||||
const email = (pb.authStore.model?.email || "").trim().toLowerCase();
|
||||
return !!email && PROD_MIGRATION_ADMIN_EMAILS.includes(email);
|
||||
};
|
||||
|
||||
export const runProdDataMigration = async () => {
|
||||
if (!pb.authStore.isValid) return;
|
||||
if (!canRunProdMigration()) {
|
||||
toast.error("Prod migration is not enabled for this account.");
|
||||
return;
|
||||
}
|
||||
|
||||
await toast.promise((async () => {
|
||||
await runLegacyTagMigration(true);
|
||||
await runPersonalContextMigration(true);
|
||||
await loadContextsIntoStore();
|
||||
await syncSupervisedContexts();
|
||||
return "Production data migration completed.";
|
||||
})(), {
|
||||
loading: "Running production data migration...",
|
||||
success: (message) => message,
|
||||
error: "Production data migration failed."
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const initStore = async () => {
|
||||
if (!pb.authStore.isValid || store.isInitializing) return;
|
||||
@@ -1744,9 +1890,11 @@ export const initStore = async () => {
|
||||
// Fire off background sync
|
||||
await backgroundSync();
|
||||
|
||||
// Run one-time migration for legacy tags
|
||||
await runLegacyTagMigration();
|
||||
await runPersonalContextMigration();
|
||||
// Run one-time migration automatically only in dev data mode.
|
||||
if (TASGRID_IS_DEV_DATA) {
|
||||
await runLegacyTagMigration();
|
||||
await runPersonalContextMigration();
|
||||
}
|
||||
|
||||
// Separate Templates load (less critical)
|
||||
try {
|
||||
|
||||
+102
-1
@@ -1,6 +1,6 @@
|
||||
import { type Component, For, Show, createSignal, lazy, Suspense } from "solid-js";
|
||||
import { ThemeToggle } from "../components/ThemeToggle";
|
||||
import { store, restoreTask, deleteTaskPermanently, restoreNote, deleteNotePermanently, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule, updateShareRule, createBucket, updateBucket, deleteBucket, toggleBucketSubscription, updateUserContextPolicy, savePersonalContextSupervisors } from "@/store";
|
||||
import { store, restoreTask, deleteTaskPermanently, restoreNote, deleteNotePermanently, addTemplate, removeTemplate, updateTemplate, upsertTagDefinition, removeTagDefinition, renameTagDefinition, addShareRule, removeShareRule, updateShareRule, createBucket, updateBucket, deleteBucket, toggleBucketSubscription, updateUserContextPolicy, savePersonalContextSupervisors, runDevDataMigration, runProdDataMigration, canRunProdMigration } from "@/store";
|
||||
import { useTheme } from "@/components/ThemeProvider";
|
||||
import { Trash2, Undo2, ArrowLeftRight, Tag, ChevronDown, ChevronRight, Share2, Users, HelpCircle, Copy, Plus, Type, Upload, Box, Edit2, Archive } from "lucide-solid";
|
||||
import { TagPicker } from "@/components/TagPicker";
|
||||
@@ -10,6 +10,7 @@ import { toast } from "solid-sonner";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { PRIORITY_OPTIONS, URGENCY_OPTIONS } from "@/lib/constants";
|
||||
import { pb } from "@/lib/pocketbase";
|
||||
import { TASGRID_IS_DEV_DATA } from "@/lib/app-config";
|
||||
import { createEffect } from "solid-js";
|
||||
|
||||
const TaskEditor = lazy(() => import("../components/TaskEditor").then(m => ({ default: m.TaskEditor })));
|
||||
@@ -23,6 +24,10 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
const [isSharingOpen, setIsSharingOpen] = createSignal(false);
|
||||
const [isImportOpen, setIsImportOpen] = createSignal(false);
|
||||
const [isBucketsOpen, setIsBucketsOpen] = createSignal(false);
|
||||
const [isDevToolsOpen, setIsDevToolsOpen] = createSignal(false);
|
||||
const [isProdMigrationOpen, setIsProdMigrationOpen] = createSignal(false);
|
||||
const [isRunningDevMigration, setIsRunningDevMigration] = createSignal(false);
|
||||
const [isRunningProdMigration, setIsRunningProdMigration] = createSignal(false);
|
||||
const [expandedTemplateId, setExpandedTemplateId] = createSignal<string | null>(null);
|
||||
const [shareUserId, setShareUserId] = createSignal("");
|
||||
const [shareType, setShareType] = createSignal<'all' | 'tag'>('tag');
|
||||
@@ -1053,6 +1058,102 @@ export const SettingsView: Component<{ setView?: (v: string) => void }> = (props
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={TASGRID_IS_DEV_DATA}>
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-xs font-bold text-muted-foreground/40 uppercase tracking-widest pl-1">Dev Tools</h2>
|
||||
<section class="p-4 sm:p-5 rounded-2xl border border-amber-500/20 bg-amber-500/5 shadow-sm space-y-4 overflow-hidden">
|
||||
<div
|
||||
class="flex items-center justify-between cursor-pointer group"
|
||||
onClick={() => setIsDevToolsOpen(!isDevToolsOpen())}
|
||||
>
|
||||
<div class="space-y-0.5">
|
||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||
<Archive size={16} />
|
||||
Dev Data Migration
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Rebuild canonical note keys, task `shareRefs` and `noteRefs`, personal user contexts, and bucket handoff cleanup for dev data.
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||
{isDevToolsOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={isDevToolsOpen()}>
|
||||
<div class="pt-2 animate-in fade-in slide-in-from-top-2 duration-300 space-y-3">
|
||||
<p class="text-[0.6875rem] text-muted-foreground">
|
||||
This is a dev-only repair tool. It updates notes and tasks in the current `*_Dev` collections to the latest key and context model.
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="w-full sm:w-auto"
|
||||
disabled={isRunningDevMigration()}
|
||||
onClick={async () => {
|
||||
setIsRunningDevMigration(true);
|
||||
try {
|
||||
await runDevDataMigration();
|
||||
} finally {
|
||||
setIsRunningDevMigration(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isRunningDevMigration() ? "Running Migration..." : "Run Dev Data Migration"}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={canRunProdMigration()}>
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-xs font-bold text-muted-foreground/40 uppercase tracking-widest pl-1">Production Migration</h2>
|
||||
<section class="p-4 sm:p-5 rounded-2xl border border-red-500/20 bg-red-500/5 shadow-sm space-y-4 overflow-hidden">
|
||||
<div
|
||||
class="flex items-center justify-between cursor-pointer group"
|
||||
onClick={() => setIsProdMigrationOpen(!isProdMigrationOpen())}
|
||||
>
|
||||
<div class="space-y-0.5">
|
||||
<h3 class="text-base font-semibold flex items-center gap-2">
|
||||
<Archive size={16} />
|
||||
Prod Data Migration
|
||||
</h3>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Admin-only one-shot migration for production tasks, notes, contexts, and note-task links.
|
||||
</p>
|
||||
</div>
|
||||
<div class="w-8 h-8 rounded-full flex items-center justify-center bg-muted/50 group-hover:bg-muted transition-colors">
|
||||
{isProdMigrationOpen() ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={isProdMigrationOpen()}>
|
||||
<div class="pt-2 animate-in fade-in slide-in-from-top-2 duration-300 space-y-3">
|
||||
<p class="text-[0.6875rem] text-muted-foreground">
|
||||
Run this only after the PocketBase production schema is ready and you have a fresh backup. This updates live production data in place.
|
||||
</p>
|
||||
<Button
|
||||
variant="destructive"
|
||||
class="w-full sm:w-auto"
|
||||
disabled={isRunningProdMigration()}
|
||||
onClick={async () => {
|
||||
setIsRunningProdMigration(true);
|
||||
try {
|
||||
await runProdDataMigration();
|
||||
} finally {
|
||||
setIsRunningProdMigration(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isRunningProdMigration() ? "Running Production Migration..." : "Run Production Migration"}
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</section>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Organization Category */}
|
||||
<div class="space-y-3">
|
||||
<h2 class="text-xs font-bold text-muted-foreground/40 uppercase tracking-widest pl-1">Organization</h2>
|
||||
|
||||
Reference in New Issue
Block a user