diff --git a/.env.example b/.env.example index b73fcaa..876b865 100644 --- a/.env.example +++ b/.env.example @@ -35,7 +35,8 @@ OPENAI_COMPAT_URL= OPENAI_COMPAT_KEY= # Feedback button — Forgejo issue filing -FORGEJO_API_TOKEN= +FORGEJO_API_TOKEN= # dev/admin token (your personal account) +FORGEJO_BOT_TOKEN= # cf-bugbot bot token — used for in-app feedback; falls back to FORGEJO_API_TOKEN FORGEJO_REPO=pyr0ball/peregrine FORGEJO_API_URL=https://git.opensourcesolarpunk.com/api/v1 # GITHUB_TOKEN= # future — enable when public mirror is active @@ -64,8 +65,21 @@ CF_ORCH_AGENT_PORT=7701 # Cloud multi-tenancy (compose.cloud.yml only — do not set for local installs) CLOUD_MODE=false CLOUD_DATA_ROOT=/devl/menagerie-data +SYNC_DB_PATH= # optional; defaults to CLOUD_DATA_ROOT/sync.db +SYNC_DB_KEY= # optional; SQLCipher key for at-rest encryption DIRECTUS_JWT_SECRET= # must match website/.env DIRECTUS_SECRET value CF_SERVER_SECRET= # random 64-char hex — generate: openssl rand -hex 32 PLATFORM_DB_URL=postgresql://cf_platform:@host.docker.internal:5433/circuitforge_platform HEIMDALL_URL=http://cf-license:8000 # internal Docker URL; override for external access HEIMDALL_ADMIN_TOKEN= # must match ADMIN_TOKEN in circuitforge-license .env + +# ── Memory (mnemo sidecar) — opt-in, requires --profile memory ─────────────── +# Launch with: docker compose --profile memory --profile up -d +# Mnemo builds a persistent knowledge graph from conversations and injects +# relevant context back into LLM prompts. Uses the ollama service as its LLM. +MNEMO_HOST=mnemo # internal service name; change for external sidecar +MNEMO_PORT=8080 +MNEMO_LLM_PROVIDER=ollama # ollama | openai | anthropic | custom +MNEMO_LLM_BASE_URL=http://ollama:11434/v1 # override for external LLM +MNEMO_LLM_API_KEY=ollama # "ollama" is a dummy value for local Ollama +MNEMO_LLM_MODEL=llama3.2:3b # must be pulled in the ollama container diff --git a/scripts/feedback_api.py b/scripts/feedback_api.py index 0c8129a..f5b5e39 100644 --- a/scripts/feedback_api.py +++ b/scripts/feedback_api.py @@ -163,7 +163,8 @@ def _ensure_labels( def create_forgejo_issue(title: str, body: str, labels: list[str]) -> dict: """Create a Forgejo issue. Returns {"number": int, "url": str}.""" - token = os.environ.get("FORGEJO_API_TOKEN", "") + # Use the bot token when set; fall back to the main API token for dev/self-hosted. + token = os.environ.get("FORGEJO_BOT_TOKEN") or os.environ.get("FORGEJO_API_TOKEN", "") repo = os.environ.get("FORGEJO_REPO", "pyr0ball/peregrine") base = os.environ.get("FORGEJO_API_URL", "https://git.opensourcesolarpunk.com/api/v1") headers = {"Authorization": f"token {token}", "Content-Type": "application/json"} @@ -183,7 +184,7 @@ def upload_attachment( issue_number: int, image_bytes: bytes, filename: str = "screenshot.png" ) -> str: """Upload a screenshot to an existing Forgejo issue. Returns attachment URL.""" - token = os.environ.get("FORGEJO_API_TOKEN", "") + token = os.environ.get("FORGEJO_BOT_TOKEN") or os.environ.get("FORGEJO_API_TOKEN", "") repo = os.environ.get("FORGEJO_REPO", "pyr0ball/peregrine") base = os.environ.get("FORGEJO_API_URL", "https://git.opensourcesolarpunk.com/api/v1") headers = {"Authorization": f"token {token}"} diff --git a/web/src/stores/settings/sync.ts b/web/src/stores/settings/sync.ts new file mode 100644 index 0000000..ba89811 --- /dev/null +++ b/web/src/stores/settings/sync.ts @@ -0,0 +1,57 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' +import { useApiFetch } from '../../composables/useApi' + +export const SYNC_DATA_CLASSES = [ + { key: 'peregrine:dismissed', label: 'Dismissed job IDs', description: 'Hides jobs you have already reviewed across devices.' }, + { key: 'peregrine:drafts', label: 'Cover letter drafts', description: 'Saves in-progress drafts so you can continue on another device.' }, +] as const + +export type SyncDataClass = typeof SYNC_DATA_CLASSES[number]['key'] + +export interface SyncPref { + data_class: string + enabled: boolean +} + +export const useSyncStore = defineStore('sync', () => { + const prefs = ref>({}) + const loading = ref(false) + const saving = ref(null) + const wiping = ref(false) + const error = ref(null) + + async function loadPrefs() { + loading.value = true + error.value = null + const { data, error: err } = await useApiFetch('/sync/prefs') + loading.value = false + if (err) { error.value = 'Failed to load sync preferences.'; return } + const map: Record = {} + for (const p of data ?? []) map[p.data_class] = p.enabled + prefs.value = map + } + + async function setPref(dataClass: string, enabled: boolean) { + saving.value = dataClass + error.value = null + const { error: err } = await useApiFetch('/sync/prefs', { + method: 'PATCH', + body: JSON.stringify({ data_class: dataClass, enabled }), + }) + saving.value = null + if (err) { error.value = `Failed to update sync preference for ${dataClass}.`; return } + prefs.value = { ...prefs.value, [dataClass]: enabled } + } + + async function wipeAll() { + wiping.value = true + error.value = null + const { error: err } = await useApiFetch('/sync/all', { method: 'DELETE' }) + wiping.value = false + if (err) { error.value = 'Failed to delete sync data.'; return } + prefs.value = {} + } + + return { prefs, loading, saving, wiping, error, loadPrefs, setPref, wipeAll } +}) diff --git a/web/src/views/settings/DataView.vue b/web/src/views/settings/DataView.vue index b8b3059..14bc6e0 100644 --- a/web/src/views/settings/DataView.vue +++ b/web/src/views/settings/DataView.vue @@ -1,7 +1,9 @@