upd calender impotz

This commit is contained in:
beo3000 2026-02-27 18:17:59 +01:00
parent 039582b0c8
commit aada965ff4
13 changed files with 1234 additions and 12 deletions

View File

@ -12,6 +12,8 @@ AZURE_TENANT_ID=<azure-ad-tenant-id>
# App Registration → Certificates & secrets → New client secret
AZURE_GRAPH_CLIENT_ID=<graph-app-registration-client-id>
AZURE_GRAPH_CLIENT_SECRET=<graph-client-secret-value>
# Fallback email when auth provides no email (e.g. API key login)
CALENDAR_USER_EMAIL=<your-email@domain.com>
# ── CLIENT (Vite — copy relevant lines to client/.env) ───────────────────────
# VITE_AZURE_CLIENT_ID=<frontend-app-registration-client-id>

View File

@ -1 +1 @@
1.1.81
1.1.84

View File

@ -16,7 +16,7 @@
import { useUnsavedGuard } from '$lib/utils/unsavedGuard.svelte';
import EventCard from './EventCard.svelte';
import { eventsForDate } from '$lib/stores/agenda';
import { createEvent, updateEventNotes } from '$lib/db/repositories';
import { createEvent, updateEventNotes, upsertContext } from '$lib/db/repositories';
import { fetchCalendarEvents, type CalendarEvent } from '$lib/utils/calendarApi';
import { extractMentionName, quoteMention } from '$lib/actions/mentionCore';
import type { PersonMeta } from '@ka-note/shared';
@ -222,6 +222,11 @@
let calendarError = $state<string | null>(null);
let calendarEvents = $state<CalendarEvent[]>([]);
// Unknown-persons dialog (shown after calendar import)
interface UnknownAttendee { name: string; email: string; selected: boolean; }
let unknownDialogOpen = $state(false);
let unknownAttendees = $state<UnknownAttendee[]>([]);
async function openCalendarPicker() {
if (!showNewEventForm) openNewEventForm();
calendarLoading = true;
@ -241,23 +246,48 @@
newEventTitle = ev.subject;
newEventTime = ev.start;
pendingBodyPreview = ev.bodyPreview;
calendarPickerOpen = false;
const persons = await db.contexts
.filter(c => !c.deletedAt && c.type === 'person')
.toArray();
const mentions = ev.attendees.map(att => {
const mentions: string[] = [];
const unknowns: UnknownAttendee[] = [];
for (const att of ev.attendees) {
const match = persons.find(
p => (p.meta as PersonMeta | null)?.email?.toLowerCase() === att.email.toLowerCase()
);
if (match) {
return quoteMention('@', extractMentionName(match));
mentions.push(quoteMention('@', extractMentionName(match)));
} else {
const cleaned = att.name.replace(/\s*\(.*?\)\s*$/, '').trim();
mentions.push(quoteMention('@', cleaned));
if (att.email) unknowns.push({ name: cleaned, email: att.email, selected: true });
}
// Strip company suffix e.g. "Lars Leifer (KRAH)" → "Lars Leifer"
const cleaned = att.name.replace(/\s*\(.*?\)\s*$/, '').trim();
return quoteMention('@', cleaned);
});
}
newEventParticipants = mentions.join(' ');
calendarPickerOpen = false;
if (unknowns.length > 0) {
unknownAttendees = unknowns;
unknownDialogOpen = true;
}
}
async function confirmUnknownPersons() {
for (const u of unknownAttendees.filter(u => u.selected)) {
const slug = u.name.toLowerCase().replace(/\s+/g, '-');
await upsertContext({
id: `u-${slug}`,
name: `Person ${u.name}`,
type: 'person',
sortOrder: 99,
meta: { fullName: u.name, email: u.email, phone: '', duSince: '' } satisfies PersonMeta,
});
}
unknownDialogOpen = false;
}
const dateEvents = $derived(eventsForDate(selectedDate));
@ -468,6 +498,32 @@
</div>
{/if}
{/if}
{#if unknownDialogOpen}
<div class="mb-3 rounded border border-[#555] bg-[#1e1e1e] p-3 shadow-lg">
<p class="mb-2 text-sm text-[#ccc]">Unbekannte Teilnehmer anlegen?</p>
<div class="mb-3 flex flex-col gap-1.5">
{#each unknownAttendees as u}
<label class="flex cursor-pointer items-center gap-2 text-sm">
<input type="checkbox" bind:checked={u.selected} class="accent-accent" />
<span class="text-white">{u.name}</span>
<span class="text-xs text-muted">{u.email}</span>
</label>
{/each}
</div>
<div class="flex gap-2">
<button
class="rounded bg-accent px-3 py-1 text-sm font-bold text-white hover:brightness-110"
onclick={confirmUnknownPersons}
>Anlegen</button>
<button
class="rounded bg-[#444] px-3 py-1 text-sm text-white hover:bg-[#555]"
onclick={() => (unknownDialogOpen = false)}
>Überspringen</button>
</div>
</div>
{/if}
{#each $dateEvents ?? [] as event (event.id)}
<div class="mb-2">
<EventCard {event} />

View File

@ -0,0 +1 @@
ALTER TABLE `api_keys` ADD `email` text NOT NULL DEFAULT '';

View File

@ -0,0 +1 @@
ALTER TABLE `api_keys` ADD `email` text DEFAULT '' NOT NULL;

File diff suppressed because it is too large Load Diff

View File

@ -99,6 +99,13 @@
"when": 1772100000000,
"tag": "0013_fts_search",
"breakpoints": true
},
{
"idx": 14,
"version": "6",
"when": 1772212044510,
"tag": "0014_classy_power_pack",
"breakpoints": true
}
]
}

Binary file not shown.

Binary file not shown.

View File

@ -144,6 +144,7 @@ export const apiKeys = sqliteTable('api_keys', {
id: text('id').primaryKey(),
userId: text('user_id').notNull(),
label: text('label').notNull(),
email: text('email').notNull().default(''),
keyHash: text('key_hash').notNull().unique(),
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
lastUsedAt: integer('last_used_at', { mode: 'timestamp' }),

View File

@ -51,7 +51,7 @@ export const authMiddleware = createMiddleware<AuthEnv>(async (c, next) => {
if (!key) {
return c.json({ error: 'Invalid or revoked API key' }, 401);
}
c.set('auth', { userId: key.userId, name: key.label, email: '' });
c.set('auth', { userId: key.userId, name: key.label, email: key.email ?? '' });
// fire-and-forget lastUsedAt update
db.update(apiKeys).set({ lastUsedAt: new Date() }).where(eq(apiKeys.id, key.id)).catch(() => {});
await next();

View File

@ -23,7 +23,7 @@ router.get('/', handle('api-keys/list', async (c) => {
}));
router.post('/', handle('api-keys/create', async (c) => {
const { userId } = c.get('auth');
const { userId, email } = c.get('auth');
const body = await c.req.json<{ label: string }>();
if (!body.label?.trim()) {
return c.json({ error: 'label required' }, 400);
@ -37,6 +37,7 @@ router.post('/', handle('api-keys/create', async (c) => {
await db.insert(apiKeys).values({
id,
userId,
email,
label: body.label.trim(),
keyHash,
createdAt: now,

View File

@ -15,7 +15,7 @@ calendar.get('/events', async (c) => {
const auth = c.get('auth');
if (!auth.email) {
return c.json({ error: 'graph_unavailable', detail: 'No email in auth context (API key or dev-bypass)' }, 502);
return c.json({ error: 'graph_unavailable', detail: 'No email in auth context' }, 502);
}
try {