From b99218c1a20d6d026a4a3d9ef2777c28b6ec59c0 Mon Sep 17 00:00:00 2001 From: alexander-wudy Date: Mon, 20 Jul 2026 01:10:12 +0200 Subject: [PATCH 1/2] fixed crypto not working on azure --- web-client/src/lib/utils.ts | 42 +++++++++++++++++++ .../src/routes/_authenticated/chat/index.tsx | 7 ++-- .../src/routes/_authenticated/notes/index.tsx | 13 +++--- 3 files changed, 53 insertions(+), 9 deletions(-) diff --git a/web-client/src/lib/utils.ts b/web-client/src/lib/utils.ts index 85eef0e..3a60f3d 100644 --- a/web-client/src/lib/utils.ts +++ b/web-client/src/lib/utils.ts @@ -9,3 +9,45 @@ import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +/** + * Generate a UUID v4 string for local-only entity IDs (e.g. checklist items + * that haven't been persisted yet). + * + * Prefers `crypto.randomUUID()` (concise, native), but falls back to a + * `crypto.getRandomValues()`-based implementation when running in a + * non-secure context — `crypto.randomUUID` is only exposed on HTTPS or + * localhost origins, so it throws on the plain-HTTP Azure deployment + * (`http://20.91.193.39/`) while the AET cluster behind a TLS-terminating + * ingress works fine. `crypto.getRandomValues` is available in every + * context (HTTP, HTTPS, file://), so the fallback keeps the app functional + * in any deployment. + */ +export function genId(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { + const bytes = new Uint8Array(16); + crypto.getRandomValues(bytes); + // RFC 4122 §4.4 — set version (4) and variant (10xx) bits. + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); + return ( + hex.slice(0, 8) + + '-' + + hex.slice(8, 12) + + '-' + + hex.slice(12, 16) + + '-' + + hex.slice(16, 20) + + '-' + + hex.slice(20, 32) + ); + } + // Last-resort fallback if `crypto` is somehow not available at all + // (very old browsers, exotic test envs). Not RFC-compliant but unique + // enough for local-item IDs in a single tab. + return `local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 11)}`; +} diff --git a/web-client/src/routes/_authenticated/chat/index.tsx b/web-client/src/routes/_authenticated/chat/index.tsx index 9951885..72af34f 100644 --- a/web-client/src/routes/_authenticated/chat/index.tsx +++ b/web-client/src/routes/_authenticated/chat/index.tsx @@ -23,6 +23,7 @@ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; import { useSendMessage, useDeleteConversation } from '#/lib/queries/chat.ts'; import { classifyChatError } from '#/lib/utils/chat.ts'; +import { genId } from '#/lib/utils'; import { getConversation } from '#/services/genai/gen-a-i/gen-a-i'; // ── Types ────────────────────────────────────────────────────── @@ -371,7 +372,7 @@ export function ChatPage() { .then((conv) => { if (conv.messages && conv.messages.length > 0) { const restored: Message[] = conv.messages.map((m) => ({ - id: crypto.randomUUID(), + id: genId(), role: m.role === 'USER' ? 'user' : 'agent', content: m.content ?? '', state: 'sent' as const, @@ -417,7 +418,7 @@ export function ChatPage() { setShowWelcome(false); - const userMsg: Message = { id: crypto.randomUUID(), role: 'user', content: trimmed, state: 'sent' }; + const userMsg: Message = { id: genId(), role: 'user', content: trimmed, state: 'sent' }; setMessages((prev) => [...prev, userMsg]); setInput(''); setIsLoading(true); @@ -435,7 +436,7 @@ export function ChatPage() { setMessages((prev) => [ ...prev, { - id: crypto.randomUUID(), + id: genId(), role: 'agent', content: data.response ?? '', state: 'sent', diff --git a/web-client/src/routes/_authenticated/notes/index.tsx b/web-client/src/routes/_authenticated/notes/index.tsx index 2103e7e..aeded34 100644 --- a/web-client/src/routes/_authenticated/notes/index.tsx +++ b/web-client/src/routes/_authenticated/notes/index.tsx @@ -45,6 +45,7 @@ import { } from '#/lib/queries/checklists.ts'; import type { IdentifiedTimestampedNote as ApiNote } from '#/types/notes'; import type { IdentifiedChecklist as ApiChecklist } from '#/types/checklist'; +import { genId } from '#/lib/utils'; // ── Types ────────────────────────────────────────────────────── @@ -54,7 +55,7 @@ type NoteType = 'note' | 'checklist'; /** * A single item within a checklist. * `id` can be a **number** (persisted via API, used for toggling completion) or a - * **string** (locally generated via `crypto.randomUUID()` for items not yet saved). + * **string** (locally generated via `genId()` for items not yet saved). * The form uses this distinction: numeric IDs map to API items, string IDs are new * items that will be created via `addChecklistItem`. */ @@ -438,7 +439,7 @@ function NoteDetail({ /** * Create / Edit form. Type is locked when editing (cannot convert note ↔ checklist). - * New checklist items get `crypto.randomUUID()` string IDs; persisted items have + * New checklist items get `genId()` string IDs; persisted items have * numeric IDs. Enter in "Add item" input triggers `addItem`. */ function NoteForm({ @@ -459,7 +460,7 @@ function NoteForm({ /** Add a new checklist item with a local UUID string ID. */ const addItem = () => { if (!newItemText.trim()) return; - setItems([...items, { id: crypto.randomUUID(), text: newItemText.trim(), done: false }]); + setItems([...items, { id: genId(), text: newItemText.trim(), done: false }]); setNewItemText(''); }; @@ -563,7 +564,7 @@ function NoteForm({ * is derived via `useMemo` from the live list to avoid stale snapshots. * * Checklist save: computes diff between original and form items — numeric IDs - * absent from form are deleted, string IDs (from `crypto.randomUUID()`) are created. + * absent from form are deleted, string IDs (from `genId()`) are created. */ export function NotesPage() { const router = useRouter(); @@ -712,7 +713,7 @@ export function NotesPage() { * calls in parallel via `Promise.all`. * **Existing checklist update**: Computes a diff against the original items: * - Items with numeric IDs in the original but absent from the form → deleted. - * - Items with string IDs (local `crypto.randomUUID()` → created via API. + * - Items with string IDs (local `genId()` → created via API. * - The checklist title is updated unconditionally. * This diff-based approach avoids deleting and recreating unchanged items, * preserving their server-side IDs and creation timestamps. @@ -781,7 +782,7 @@ export function NotesPage() { ); } - // Add new items (string ids from crypto.randomUUID) + // Add new items (string ids from genId) const newItems = note.checklist.filter((item) => typeof item.id === 'string'); if (newItems.length > 0) { await Promise.all( From ad9e8e28754a6752dd63f7781d792a92bd2fdea5 Mon Sep 17 00:00:00 2001 From: Alexander Michael Wudy Date: Mon, 20 Jul 2026 01:39:01 +0200 Subject: [PATCH 2/2] add gemini key to azure deployment --- .github/workflows/deploy-azure.yml | 1 + infra/iac/azure/playbook.yml | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/deploy-azure.yml b/.github/workflows/deploy-azure.yml index 2d7701f..18096b5 100644 --- a/.github/workflows/deploy-azure.yml +++ b/.github/workflows/deploy-azure.yml @@ -17,6 +17,7 @@ jobs: ARM_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }} steps: - name: Checkout Code diff --git a/infra/iac/azure/playbook.yml b/infra/iac/azure/playbook.yml index 2a516ba..b7553a4 100644 --- a/infra/iac/azure/playbook.yml +++ b/infra/iac/azure/playbook.yml @@ -103,6 +103,13 @@ line: 'DEFAULT_LLM_MODEL=gemini' state: present + - name: Inject hosted LLM API key into deployed .env + ansible.builtin.lineinfile: + path: /home/devops-admin/app/.env + regexp: '^GEMINI_API_KEY=.*$' + line: "GEMINI_API_KEY={{ lookup('env', 'GEMINI_API_KEY') }}" + state: present + - name: Ensure init-db directory exists on VM ansible.builtin.file: path: /home/devops-admin/app/init-db