feat(db): mesh data model — meshes, members, invites, audit log

- pgSchema "mesh" with 4 tables isolating the peer mesh domain
- Enums: visibility, transport, tier, role
- audit_log is metadata-only (E2E encryption enforced at broker/client)
- Cascade on mesh delete, soft-delete via archivedAt/revokedAt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-04 21:19:32 +01:00
commit d3163a5bff
1384 changed files with 314925 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
"use client";
import { AspectRatio } from "@turbostarter/ai/image/types";
import { useTranslation } from "@turbostarter/i18n";
import { FormControl, FormField, FormItem } from "@turbostarter/ui-web/form";
import { Icons } from "@turbostarter/ui-web/icons";
import {
Select,
SelectContent,
SelectItem,
SelectPortal,
SelectTrigger,
SelectValue,
} from "@turbostarter/ui-web/select";
import type { Control, FieldValues, Path } from "react-hook-form";
const icons = {
[AspectRatio.SQUARE]: Icons.Square,
[AspectRatio.STANDARD]: Icons.Square,
[AspectRatio.LANDSCAPE]: Icons.RectangleHorizontal,
[AspectRatio.PORTRAIT]: Icons.RectangleVertical,
};
interface AspectSelectorProps<T extends FieldValues> {
readonly control: Control<T>;
readonly name: Path<T>;
readonly options: readonly {
readonly id: AspectRatio;
readonly value: string;
}[];
}
export const AspectSelector = <T extends FieldValues>({
name,
control,
options,
}: AspectSelectorProps<T>) => {
const { t } = useTranslation("common");
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem className="max-w-24 min-w-0 @sm:max-w-none">
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger size="sm">
<SelectValue />
</SelectTrigger>
<SelectPortal>
<SelectContent align="end">
{options.map(({ id, value }) => {
const Icon = icons[id];
return (
<SelectItem key={id} value={id}>
<div className="flex items-center gap-2.5">
<Icon className="size-4 shrink-0" />
<span className="min-w-0 truncate font-medium">
<span className="hidden @lg:inline">
{t(
id.toLowerCase() as Lowercase<
keyof typeof AspectRatio
>,
)}{" "}
</span>
<span>{`(${value})`}</span>
</span>
</div>
</SelectItem>
);
})}
</SelectContent>
</SelectPortal>
</Select>
</FormControl>
</FormItem>
)}
/>
);
};

View File

@@ -0,0 +1,67 @@
"use client";
import { FormControl, FormField, FormItem } from "@turbostarter/ui-web/form";
import { Icons } from "@turbostarter/ui-web/icons";
import {
Select,
SelectContent,
SelectItem,
SelectPortal,
SelectTrigger,
SelectValue,
} from "@turbostarter/ui-web/select";
import type { Control, FieldValues, Path } from "react-hook-form";
interface ImageCountSelectorProps<T extends FieldValues> {
readonly control: Control<T>;
readonly name: Path<T>;
readonly min?: number;
readonly max?: number;
}
export const ImageCountSelector = <T extends FieldValues>({
name,
control,
min = 1,
max = 5,
}: ImageCountSelectorProps<T>) => {
return (
<FormField
control={control}
name={name}
render={({ field }) => (
<FormItem className="min-w-0 gap-0">
<FormControl>
<Select
value={`${field.value}`}
onValueChange={(value) => field.onChange(parseInt(value, 10))}
>
<SelectTrigger size="sm">
<div className="flex items-center gap-2">
<Icons.Image className="size-4 shrink-0" />
<SelectValue />
</div>
</SelectTrigger>
<SelectPortal>
<SelectContent align="end">
{Array.from({ length: max - min + 1 }, (_, i) => i + min).map(
(count) => (
<SelectItem key={count} value={count.toString()}>
<div className="flex items-center gap-2.5">
<span className="min-w-0 truncate font-medium">
{count}
</span>
</div>
</SelectItem>
),
)}
</SelectContent>
</SelectPortal>
</Select>
</FormControl>
</FormItem>
)}
/>
);
};

View File

@@ -0,0 +1,103 @@
"use client";
import { useEffect } from "react";
import { MODELS } from "@turbostarter/ai/image/constants";
import { useTranslation } from "@turbostarter/i18n";
import { Button } from "@turbostarter/ui-web/button";
import {
Form,
FormControl,
FormField,
FormItem,
} from "@turbostarter/ui-web/form";
import { Icons } from "@turbostarter/ui-web/icons";
import { Composer } from "~/modules/common/ai/composer";
import { ModelSelector } from "~/modules/common/ai/composer/model-selector";
import { AspectSelector } from "./aspect-selector";
import { ImageCountSelector } from "./image-count-selector";
import { useComposer } from "./use-composer";
interface ImageComposerProps {
id?: string;
prompt?: string;
reset?: () => void;
}
export const ImageComposer = ({
id,
prompt: initialPrompt,
reset,
}: ImageComposerProps) => {
const { t } = useTranslation(["ai", "common"]);
const { form, model, onSubmit } = useComposer({ id });
const prompt = form.watch("prompt");
useEffect(() => {
if (initialPrompt) {
form.setValue("prompt", initialPrompt);
form.setFocus("prompt");
reset?.();
}
}, [initialPrompt, form, reset]);
return (
<Form {...form}>
<Composer.Form onSubmit={form.handleSubmit(onSubmit)}>
<Composer.Input className="pb-12">
<FormField
control={form.control}
name="prompt"
render={({ field }) => (
<FormItem>
<FormControl>
<Composer.Textarea
{...field}
autoFocus
maxLength={5_000}
placeholder={t("image.composer.placeholder")}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
return form.handleSubmit(onSubmit)();
}
}}
/>
</FormControl>
</FormItem>
)}
/>
<div className="absolute inset-x-0 bottom-0 flex w-full gap-1.5 overflow-hidden border-2 border-transparent p-2 @[480px]/input:p-3">
<div className="flex max-w-full grow gap-px">
<ImageCountSelector control={form.control} name="options.count" />
<AspectSelector
control={form.control}
name="options.aspectRatio"
options={MODELS.find((m) => m.id === model)?.dimensions ?? []}
/>
</div>
<ModelSelector
control={form.control}
name="options.model"
options={MODELS}
/>
<Button
className="ml-auto shrink-0 rounded-full"
disabled={!prompt.trim()}
size="icon"
type="submit"
>
<Icons.ImagePlay className="size-5" />
</Button>
</div>
</Composer.Input>
</Composer.Form>
</Form>
);
};

View File

@@ -0,0 +1,121 @@
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { useCallback, useEffect, useRef } from "react";
import { useForm, useFormContext } from "react-hook-form";
import { create } from "zustand";
import { persist } from "zustand/middleware";
import { MODELS } from "@turbostarter/ai/image/constants";
import { imageGenerationSchema } from "@turbostarter/ai/image/schema";
import { useDebounceCallback } from "@turbostarter/shared/hooks";
import { generateId } from "@turbostarter/shared/utils";
import { useImageGeneration } from "~/modules/image/use-image-generation";
import type {
ImageGenerationOptionsPayload,
ImageGenerationPayload,
} from "@turbostarter/ai/image/schema";
import type { WatchObserver } from "react-hook-form";
interface ImageComposerState {
prompt: string;
options: ImageGenerationOptionsPayload;
setPrompt: (prompt: string) => void;
setOptions: (options: Partial<ImageGenerationOptionsPayload>) => void;
reset: () => void;
}
const DEFAULT_OPTIONS = {
model: MODELS[0].id,
aspectRatio: MODELS[0].dimensions[0].id,
count: 1,
};
const useImageComposerStore = create<ImageComposerState>()(
persist(
(set) => ({
prompt: "",
options: DEFAULT_OPTIONS,
setPrompt: (prompt) => set({ prompt }),
setOptions: (options) =>
set((state) => ({
options: { ...state.options, ...options },
})),
reset: () =>
set({
prompt: "",
options: DEFAULT_OPTIONS,
}),
}),
{
name: "image-options",
partialize: (state) => ({ options: state.options }),
},
),
);
interface UseComposerProps {
id?: string;
}
export const useComposer = ({ id: passedId }: UseComposerProps = {}) => {
const { prompt, options, setPrompt, setOptions, reset } =
useImageComposerStore();
const currentId = useRef(passedId);
const { createGeneration } = useImageGeneration({
id: currentId.current,
});
useEffect(() => {
if (currentId.current !== passedId) {
reset();
currentId.current = passedId ?? generateId();
}
}, [passedId, reset]);
const newForm = useForm({
resolver: zodResolver(imageGenerationSchema),
defaultValues: {
prompt,
options,
},
});
const contextForm = useFormContext<ImageGenerationPayload>();
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const form = contextForm ?? newForm;
const model = form.watch("options.model");
const sync: WatchObserver<ImageGenerationPayload> = useCallback(
(values) => {
setPrompt(values.prompt ?? "");
setOptions(values.options ?? DEFAULT_OPTIONS);
},
[setOptions, setPrompt],
);
const debouncedSync = useDebounceCallback(sync, 500, {
leading: true,
});
useEffect(() => {
const subscription = form.watch(debouncedSync);
return () => subscription.unsubscribe();
}, [form, debouncedSync]);
const onSubmit = (input: ImageGenerationPayload) => {
form.resetField("prompt");
createGeneration.mutate(input);
};
return {
form,
model,
prompt,
onSubmit,
reset,
};
};