feat: journal dashboard widgets, agent polish, and edit-mode live previews
CI / checks (push) Failing after 2m7s
CI / build (push) Successful in 4m36s

Journal dashboard widgets and quick-add; rich-text quick-add dialogs.

Dashboard draft sync for live edit previews; assistant bubble + API tools.

Journal UX: stress slider, mood grid, query cap fix.
This commit is contained in:
ginnoir
2026-07-04 22:03:45 -05:00
parent 4a924a4107
commit a09747c314
76 changed files with 4594 additions and 611 deletions
+1
View File
@@ -35,6 +35,7 @@ export const users = pgTable("users", {
notifPush: boolean("notif_push").notNull().default(true),
notifInApp: boolean("notif_inapp").notNull().default(true),
notifNtfy: boolean("notif_ntfy").notNull().default(false),
assistantEnabled: boolean("assistant_enabled").notNull().default(false),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
});
+133
View File
@@ -0,0 +1,133 @@
import { createHash, randomBytes } from "crypto";
import { and, eq, isNull } from "drizzle-orm";
import type { ApiAuthContext } from "@/lib/api-auth";
import { db } from "@/lib/db";
import { getEntityType, getRegistry } from "./registry";
import { shareLinks } from "./schema";
import { ensureEntityShareAuthorized } from "./share-authorization";
import type { ShareLinkCapabilities } from "./share";
export type CreateShareLinkResult = {
url: string;
token: string;
expiresAt: Date | null;
};
export type EntityShareLink = {
id: string;
entityType: string;
entityId: string;
createdAt: string;
expiresAt: string | null;
capabilities: ShareLinkCapabilities;
};
function hashToken(raw: string): string {
return createHash("sha256").update(raw).digest("hex");
}
function buildUrl(token: string): string {
const base =
process.env["NEXT_PUBLIC_APP_URL"] ?? process.env["AUTH_URL"] ?? "http://localhost:3000";
return `${base}/s/${token}`;
}
export function listShareableEntityTypes() {
return getRegistry()
.entityTypes.filter((entity) => entity.share?.canShare)
.map((entity) => ({
type: entity.type,
label: entity.label.singular,
defaultCapabilities: entity.share?.defaultCapabilities ?? ["read"],
}));
}
export async function createShareLinkForScope(
scope: ApiAuthContext,
entityType: string,
entityId: string,
opts: {
expiresAt?: Date | null;
capabilities?: Partial<ShareLinkCapabilities>;
} = {},
): Promise<CreateShareLinkResult> {
const registration = getEntityType(entityType);
if (!registration?.share?.canShare) {
throw new Error(`Entity type "${entityType}" is not shareable`);
}
if (!scope.userId) {
throw new Error("Share links require an authenticated user session");
}
await ensureEntityShareAuthorized(registration, entityId, {
householdId: scope.householdId,
userId: scope.userId,
});
const rawToken = randomBytes(32).toString("base64url");
const tokenHash = hashToken(rawToken);
const capabilities: ShareLinkCapabilities = {
read: opts.capabilities?.read ?? true,
write: opts.capabilities?.write ?? false,
};
await db.insert(shareLinks).values({
householdId: scope.householdId,
entityType,
entityId,
token: tokenHash,
capabilities,
createdBy: scope.userId,
expiresAt: opts.expiresAt ?? null,
});
return { url: buildUrl(rawToken), token: rawToken, expiresAt: opts.expiresAt ?? null };
}
export async function revokeShareLinkForScope(scope: ApiAuthContext, id: string): Promise<void> {
const [row] = await db
.select({ id: shareLinks.id })
.from(shareLinks)
.where(and(eq(shareLinks.id, id), eq(shareLinks.householdId, scope.householdId)))
.limit(1);
if (!row) throw new Error("Share link not found");
await db.update(shareLinks).set({ revokedAt: new Date() }).where(eq(shareLinks.id, id));
}
export async function listShareLinksForScope(
scope: ApiAuthContext,
filters?: { entityType?: string; entityId?: string },
): Promise<EntityShareLink[]> {
const now = new Date();
const conditions = [eq(shareLinks.householdId, scope.householdId), isNull(shareLinks.revokedAt)];
if (filters?.entityType) conditions.push(eq(shareLinks.entityType, filters.entityType));
if (filters?.entityId) conditions.push(eq(shareLinks.entityId, filters.entityId));
const rows = await db
.select({
id: shareLinks.id,
entityType: shareLinks.entityType,
entityId: shareLinks.entityId,
createdAt: shareLinks.createdAt,
expiresAt: shareLinks.expiresAt,
capabilities: shareLinks.capabilities,
})
.from(shareLinks)
.where(and(...conditions))
.orderBy(shareLinks.createdAt);
return rows
.filter((row) => !row.expiresAt || row.expiresAt > now)
.map((row) => ({
id: row.id,
entityType: row.entityType,
entityId: row.entityId,
createdAt: row.createdAt.toISOString(),
expiresAt: row.expiresAt?.toISOString() ?? null,
capabilities: row.capabilities,
}));
}