Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/deploy-azure.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions infra/iac/azure/playbook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 42 additions & 0 deletions web-client/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)}`;
}
7 changes: 4 additions & 3 deletions web-client/src/routes/_authenticated/chat/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────────────────────────────────────────
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -435,7 +436,7 @@ export function ChatPage() {
setMessages((prev) => [
...prev,
{
id: crypto.randomUUID(),
id: genId(),
role: 'agent',
content: data.response ?? '',
state: 'sent',
Expand Down
13 changes: 7 additions & 6 deletions web-client/src/routes/_authenticated/notes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────────────────────────────────────────

Expand All @@ -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`.
*/
Expand Down Expand Up @@ -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({
Expand All @@ -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('');
};

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down
Loading