chore: Various improvements and CI setup

- Add Jenkinsfile for CI/CD pipeline
- Fix keyboard shortcut '?' handling for help dialog
- Update note form and connections components
- Add work mode toggle improvements
- Update navigation history and usage tracking
- Improve validators
- Add session summaries
This commit is contained in:
2026-03-23 22:25:36 -03:00
parent e0433f8e57
commit ece8163d15
12 changed files with 976 additions and 36 deletions

View File

@@ -13,7 +13,14 @@ export function getNavigationHistory(): NavigationEntry[] {
try {
const stored = localStorage.getItem(NAVIGATION_HISTORY_KEY)
if (!stored) return []
return JSON.parse(stored)
const entries: NavigationEntry[] = JSON.parse(stored)
// Deduplicate by noteId, keeping the first occurrence (most recent)
const seen = new Set<string>()
return entries.filter(entry => {
if (seen.has(entry.noteId)) return false
seen.add(entry.noteId)
return true
})
} catch {
return []
}

View File

@@ -176,12 +176,18 @@ export async function getCoUsedNotes(
updatedAt: { gte: since },
},
orderBy: { weight: 'desc' },
take: limit,
take: limit * 2, // Fetch more to account for duplicates we'll filter
})
// Deduplicate by relatedNoteId - only keep highest weight per note
const seenIds = new Set<string>()
const result: { noteId: string; title: string; type: string; weight: number }[] = []
for (const cu of coUsages) {
const relatedNoteId = cu.fromNoteId === noteId ? cu.toNoteId : cu.fromNoteId
if (seenIds.has(relatedNoteId)) continue
seenIds.add(relatedNoteId)
const note = await prisma.note.findUnique({
where: { id: relatedNoteId },
select: { id: true, title: true, type: true },
@@ -194,6 +200,7 @@ export async function getCoUsedNotes(
weight: cu.weight,
})
}
if (result.length >= limit) break
}
return result
} catch {

View File

@@ -4,8 +4,9 @@ export const NoteTypeEnum = z.enum(['command', 'snippet', 'decision', 'recipe',
export const CreationSourceEnum = z.enum(['form', 'quick', 'import'])
export const noteSchema = z.object({
id: z.string().optional(),
// Base note schema without transform - for use with partial()
const baseNoteSchema = z.object({
id: z.string().optional().nullable(),
title: z.string().min(1, 'Title is required').max(200),
content: z.string().min(1, 'Content is required'),
type: NoteTypeEnum.default('note'),
@@ -15,10 +16,18 @@ export const noteSchema = z.object({
creationSource: CreationSourceEnum.default('form'),
})
export const updateNoteSchema = noteSchema.partial().extend({
id: z.string(),
// Transform to remove id if null/undefined (for creation)
export const noteSchema = baseNoteSchema.transform(data => {
if (data.id == null) {
const { id, ...rest } = data
return rest
}
return data
})
// For update, use partial of base schema with optional id (id comes from URL path, not body)
export const updateNoteSchema = baseNoteSchema.partial()
export const searchSchema = z.object({
q: z.string().optional(),
type: NoteTypeEnum.optional(),