Compare commits
No commits in common. "95e76edaeaad8650413eacc40fdcf5b6af935b2e" and "935071951656cc57c646c1e778482d5b70dead37" have entirely different histories.
95e76edaea
...
9350719516
10 changed files with 8 additions and 261 deletions
|
|
@ -1,5 +0,0 @@
|
||||||
# app/api/endpoints/corrections.py — user corrections to LLM output for SFT training
|
|
||||||
from circuitforge_core.api import make_corrections_router
|
|
||||||
from app.db.session import get_db
|
|
||||||
|
|
||||||
router = make_corrections_router(get_db=get_db, product="kiwi")
|
|
||||||
|
|
@ -17,7 +17,6 @@ from app.models.schemas.saved_recipe import (
|
||||||
SaveRecipeRequest,
|
SaveRecipeRequest,
|
||||||
UpdateSavedRecipeRequest,
|
UpdateSavedRecipeRequest,
|
||||||
)
|
)
|
||||||
from app.services.magpie_hook import fire_recipe_signal
|
|
||||||
from app.tiers import can_use
|
from app.tiers import can_use
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -61,9 +60,7 @@ async def save_recipe(
|
||||||
row = store.save_recipe(req.recipe_id, req.notes, req.rating)
|
row = store.save_recipe(req.recipe_id, req.notes, req.rating)
|
||||||
return _to_summary(row, store)
|
return _to_summary(row, store)
|
||||||
|
|
||||||
result = await asyncio.to_thread(_in_thread, session.db, _run)
|
return await asyncio.to_thread(_in_thread, session.db, _run)
|
||||||
asyncio.create_task(fire_recipe_signal(session.db, req.recipe_id, req.rating, []))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/{recipe_id}", status_code=204)
|
@router.delete("/{recipe_id}", status_code=204)
|
||||||
|
|
@ -90,11 +87,7 @@ async def update_saved_recipe(
|
||||||
)
|
)
|
||||||
return _to_summary(row, store)
|
return _to_summary(row, store)
|
||||||
|
|
||||||
result = await asyncio.to_thread(_in_thread, session.db, _run)
|
return await asyncio.to_thread(_in_thread, session.db, _run)
|
||||||
asyncio.create_task(
|
|
||||||
fire_recipe_signal(session.db, recipe_id, req.rating, req.style_tags or [])
|
|
||||||
)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[SavedRecipeSummary])
|
@router.get("", response_model=list[SavedRecipeSummary])
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
from app.api.endpoints import health, receipts, export, inventory, ocr, recipes, settings, staples, feedback, feedback_attach, household, saved_recipes, imitate, meal_plans, orch_usage, session, shopping
|
from app.api.endpoints import health, receipts, export, inventory, ocr, recipes, settings, staples, feedback, feedback_attach, household, saved_recipes, imitate, meal_plans, orch_usage, session, shopping
|
||||||
from app.api.endpoints.community import router as community_router
|
from app.api.endpoints.community import router as community_router
|
||||||
from app.api.endpoints.corrections import router as corrections_router
|
|
||||||
from app.api.endpoints.recipe_tags import router as recipe_tags_router
|
from app.api.endpoints.recipe_tags import router as recipe_tags_router
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
|
|
@ -25,4 +24,3 @@ api_router.include_router(orch_usage.router, prefix="/orch-usage", tags=
|
||||||
api_router.include_router(shopping.router, prefix="/shopping", tags=["shopping"])
|
api_router.include_router(shopping.router, prefix="/shopping", tags=["shopping"])
|
||||||
api_router.include_router(community_router)
|
api_router.include_router(community_router)
|
||||||
api_router.include_router(recipe_tags_router)
|
api_router.include_router(recipe_tags_router)
|
||||||
api_router.include_router(corrections_router, prefix="/corrections", tags=["corrections"])
|
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,6 @@ class Settings:
|
||||||
os.environ.get("BROWSE_COUNTS_PATH", str(DATA_DIR / "browse_counts.db"))
|
os.environ.get("BROWSE_COUNTS_PATH", str(DATA_DIR / "browse_counts.db"))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Magpie data flywheel — ingest endpoint for anonymized recipe signals
|
|
||||||
# Set MAGPIE_INGEST_URL to enable; leave unset (or None) to disable silently.
|
|
||||||
MAGPIE_INGEST_URL: str | None = os.environ.get("MAGPIE_INGEST_URL") or None
|
|
||||||
|
|
||||||
# Community feature settings
|
# Community feature settings
|
||||||
COMMUNITY_DB_URL: str | None = os.environ.get("COMMUNITY_DB_URL") or None
|
COMMUNITY_DB_URL: str | None = os.environ.get("COMMUNITY_DB_URL") or None
|
||||||
COMMUNITY_PSEUDONYM_SALT: str = os.environ.get(
|
COMMUNITY_PSEUDONYM_SALT: str = os.environ.get(
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
-- 040_corrections.sql — corrections table for SFT training data
|
|
||||||
-- Schema from circuitforge_core.api.corrections.CORRECTIONS_MIGRATION_SQL
|
|
||||||
CREATE TABLE IF NOT EXISTS corrections (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
item_id TEXT NOT NULL DEFAULT '',
|
|
||||||
product TEXT NOT NULL,
|
|
||||||
correction_type TEXT NOT NULL,
|
|
||||||
input_text TEXT NOT NULL,
|
|
||||||
original_output TEXT NOT NULL,
|
|
||||||
corrected_output TEXT NOT NULL DEFAULT '',
|
|
||||||
rating TEXT NOT NULL DEFAULT 'down',
|
|
||||||
context TEXT NOT NULL DEFAULT '{}',
|
|
||||||
opted_in INTEGER NOT NULL DEFAULT 0,
|
|
||||||
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_corrections_product
|
|
||||||
ON corrections (product);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_corrections_opted_in
|
|
||||||
ON corrections (opted_in);
|
|
||||||
|
|
@ -6,8 +6,6 @@ Cloud mode: opens a Store at the per-user DB path from the CloudUser session.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
from collections.abc import Iterator
|
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
@ -23,16 +21,3 @@ def get_store(session: CloudUser = Depends(get_session)) -> Generator[Store, Non
|
||||||
yield store
|
yield store
|
||||||
finally:
|
finally:
|
||||||
store.close()
|
store.close()
|
||||||
|
|
||||||
|
|
||||||
def get_db(session: CloudUser = Depends(get_session)) -> Iterator[sqlite3.Connection]:
|
|
||||||
"""FastAPI dependency — yields the raw sqlite3.Connection for the current user.
|
|
||||||
|
|
||||||
Used by make_corrections_router() from circuitforge-core, which expects a
|
|
||||||
dependency that yields a sqlite3.Connection directly.
|
|
||||||
"""
|
|
||||||
store = Store(session.db)
|
|
||||||
try:
|
|
||||||
yield store.conn
|
|
||||||
finally:
|
|
||||||
store.close()
|
|
||||||
|
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
"""Magpie data-flywheel hook.
|
|
||||||
|
|
||||||
Fires anonymized recipe-signal events to the Magpie ingest endpoint when a
|
|
||||||
user saves or rates a recipe. This is the Kiwi side of the flywheel — Magpie
|
|
||||||
does not have a receiver endpoint yet, so the hook stubs out gracefully: if
|
|
||||||
``MAGPIE_INGEST_URL`` is unset, or the request fails for any reason, it logs
|
|
||||||
at DEBUG level and returns without raising.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
_INGEST_PATH = "/api/v1/ingest/recipe-signal"
|
|
||||||
|
|
||||||
|
|
||||||
async def fire_recipe_signal(
|
|
||||||
db_path: Path,
|
|
||||||
recipe_id: int,
|
|
||||||
rating: int | None,
|
|
||||||
style_tags: list[str],
|
|
||||||
) -> None:
|
|
||||||
"""Post an anonymized recipe signal to Magpie if the user has opted in.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db_path: Path to the user's SQLite database.
|
|
||||||
recipe_id: Internal Kiwi recipe ID being rated/saved.
|
|
||||||
rating: Star rating (0–5) or None if not yet rated.
|
|
||||||
style_tags: Style tags applied to the saved recipe.
|
|
||||||
"""
|
|
||||||
from app.core.config import settings
|
|
||||||
|
|
||||||
if not settings.MAGPIE_INGEST_URL:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check per-user opt-in via a short-lived Store (own connection, own thread
|
|
||||||
# context is fine — this runs in the async event loop as a background task
|
|
||||||
# so we open and close the connection immediately).
|
|
||||||
from app.db.store import Store
|
|
||||||
|
|
||||||
try:
|
|
||||||
store = Store(db_path)
|
|
||||||
try:
|
|
||||||
opt_in = store.get_setting("magpie_opt_in")
|
|
||||||
finally:
|
|
||||||
store.close()
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
logger.debug("magpie_hook: could not read magpie_opt_in setting: %s", exc)
|
|
||||||
return
|
|
||||||
|
|
||||||
if opt_in != "true":
|
|
||||||
return
|
|
||||||
|
|
||||||
# Fetch the recipe to get its external_id (source URL slug / corpus key).
|
|
||||||
try:
|
|
||||||
store = Store(db_path)
|
|
||||||
try:
|
|
||||||
recipe = store.get_recipe(recipe_id)
|
|
||||||
finally:
|
|
||||||
store.close()
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
logger.debug("magpie_hook: could not fetch recipe %d: %s", recipe_id, exc)
|
|
||||||
return
|
|
||||||
|
|
||||||
if recipe is None:
|
|
||||||
logger.debug("magpie_hook: recipe %d not found, skipping", recipe_id)
|
|
||||||
return
|
|
||||||
|
|
||||||
external_id: str | None = recipe.get("external_id") if isinstance(recipe, dict) else getattr(recipe, "external_id", None)
|
|
||||||
if not external_id:
|
|
||||||
# Corpus recipe not yet enriched with a source identifier — skip quietly.
|
|
||||||
logger.debug("magpie_hook: recipe %d has no external_id, skipping", recipe_id)
|
|
||||||
return
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"product": "kiwi",
|
|
||||||
"signal": "recipe_rating",
|
|
||||||
"external_id": external_id,
|
|
||||||
"rating": rating,
|
|
||||||
"style_tags": style_tags,
|
|
||||||
}
|
|
||||||
|
|
||||||
url = settings.MAGPIE_INGEST_URL.rstrip("/") + _INGEST_PATH
|
|
||||||
|
|
||||||
try:
|
|
||||||
import httpx
|
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=3.0) as client:
|
|
||||||
response = await client.post(url, json=payload)
|
|
||||||
logger.debug(
|
|
||||||
"magpie_hook: POST %s → %d", url, response.status_code
|
|
||||||
)
|
|
||||||
except Exception as exc: # noqa: BLE001
|
|
||||||
# Magpie may not have a receiver yet — log and swallow.
|
|
||||||
logger.debug("magpie_hook: ingest request failed (stub): %s", exc)
|
|
||||||
|
|
@ -527,10 +527,8 @@ function onTagSearchInput() {
|
||||||
tagSearchDebounce = setTimeout(async () => {
|
tagSearchDebounce = setTimeout(async () => {
|
||||||
tagModal.value.searching = true
|
tagModal.value.searching = true
|
||||||
try {
|
try {
|
||||||
// Use the first available domain with category=_all to search all recipes by title.
|
// Re-use the browser API: browse all recipes filtered by title substring
|
||||||
// Domain must be a real domain slug — '_all' is not valid at the browse endpoint.
|
const res = await browserAPI.browse('_all', '_all', { page: 1, q })
|
||||||
const searchDomain = domains.value[0]?.id ?? 'cuisine'
|
|
||||||
const res = await browserAPI.browse(searchDomain, '_all', { page: 1, q })
|
|
||||||
tagModal.value.results = (res.recipes ?? []).slice(0, 8).map(
|
tagModal.value.results = (res.recipes ?? []).slice(0, 8).map(
|
||||||
(r: { id: number; title: string }) => ({ id: r.id, title: r.title })
|
(r: { id: number; title: string }) => ({ id: r.id, title: r.title })
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -225,23 +225,6 @@
|
||||||
</ol>
|
</ol>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<!-- Community tags — accepted location tags from other users -->
|
|
||||||
<div v-if="communityTags.length > 0" class="detail-section community-tags-section">
|
|
||||||
<h3 class="section-label">Community categories</h3>
|
|
||||||
<div class="community-tags-list">
|
|
||||||
<span
|
|
||||||
v-for="tag in communityTags"
|
|
||||||
:key="tag.id"
|
|
||||||
class="community-tag-chip"
|
|
||||||
:class="{ 'community-tag-chip--accepted': tag.accepted }"
|
|
||||||
:title="tag.accepted ? 'Confirmed by the community' : 'Pending confirmation'"
|
|
||||||
>
|
|
||||||
{{ tag.domain }} › {{ tag.category }}<template v-if="tag.subcategory"> › {{ tag.subcategory }}</template>
|
|
||||||
<span v-if="tag.accepted" class="community-tag-check" aria-label="Confirmed">✓</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bottom padding so last step isn't hidden behind sticky footer -->
|
<!-- Bottom padding so last step isn't hidden behind sticky footer -->
|
||||||
<div style="height: var(--spacing-xl)" />
|
<div style="height: var(--spacing-xl)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -371,7 +354,7 @@
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
||||||
import { useRecipesStore } from '../stores/recipes'
|
import { useRecipesStore } from '../stores/recipes'
|
||||||
import { useSavedRecipesStore } from '../stores/savedRecipes'
|
import { useSavedRecipesStore } from '../stores/savedRecipes'
|
||||||
import { inventoryAPI, recipesAPI, browserAPI } from '../services/api'
|
import { inventoryAPI, recipesAPI } from '../services/api'
|
||||||
import type { RecipeSuggestion, GroceryLink, StepAnalysis } from '../services/api'
|
import type { RecipeSuggestion, GroceryLink, StepAnalysis } from '../services/api'
|
||||||
import SaveRecipeModal from './SaveRecipeModal.vue'
|
import SaveRecipeModal from './SaveRecipeModal.vue'
|
||||||
|
|
||||||
|
|
@ -403,12 +386,6 @@ onMounted(() => {
|
||||||
)
|
)
|
||||||
;(focusable ?? dialogRef.value)?.focus()
|
;(focusable ?? dialogRef.value)?.focus()
|
||||||
})
|
})
|
||||||
// Load community tags in the background — non-critical, silently skip on error
|
|
||||||
browserAPI.listRecipeTags(props.recipe.id).then((tags) => {
|
|
||||||
communityTags.value = tags
|
|
||||||
}).catch(() => {
|
|
||||||
// Community tags are supplemental; silently skip on error
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|
@ -434,10 +411,6 @@ const isSaved = computed(() => savedStore.isSaved(props.recipe.id))
|
||||||
|
|
||||||
const cookDone = ref(false)
|
const cookDone = ref(false)
|
||||||
|
|
||||||
// ── Community tags ────────────────────────────────────────
|
|
||||||
type CommunityTag = { id: number; domain: string; category: string; subcategory: string | null; pseudonym: string; upvotes: number; accepted: boolean }
|
|
||||||
const communityTags = ref<CommunityTag[]>([])
|
|
||||||
|
|
||||||
// ── Leftover shelf-life ────────────────────────────────────
|
// ── Leftover shelf-life ────────────────────────────────────
|
||||||
type LeftoversData = { fridge_days: number; freeze_days: number | null; freeze_by_day: number | null; storage_advice: string }
|
type LeftoversData = { fridge_days: number; freeze_days: number | null; freeze_by_day: number | null; storage_advice: string }
|
||||||
const leftovers = ref<LeftoversData | null>(null)
|
const leftovers = ref<LeftoversData | null>(null)
|
||||||
|
|
@ -1655,39 +1628,4 @@ details[open].steps-collapsible .steps-collapsible-summary::before {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Community tags section ──────────────────────────────── */
|
|
||||||
.community-tags-section {
|
|
||||||
padding-top: var(--spacing-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-tags-list {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--spacing-xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-tag-chip {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
padding: 2px var(--spacing-sm);
|
|
||||||
border-radius: var(--radius-pill, 999px);
|
|
||||||
font-size: var(--font-size-xs, 0.72rem);
|
|
||||||
background: var(--color-bg-secondary);
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-tag-chip--accepted {
|
|
||||||
background: rgba(124, 111, 205, 0.12);
|
|
||||||
color: var(--color-accent, #7c6fcd);
|
|
||||||
border-color: rgba(124, 111, 205, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.community-tag-check {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -271,24 +271,6 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Data Sharing (cloud only) -->
|
|
||||||
<section v-if="isCloudMode" class="mt-md">
|
|
||||||
<h3 class="text-lg font-semibold mb-xs">Data Sharing</h3>
|
|
||||||
<label class="data-sharing-toggle flex-start gap-sm text-sm">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
:checked="magpieOptIn"
|
|
||||||
@change="setMagpieOptIn(($event.target as HTMLInputElement).checked)"
|
|
||||||
/>
|
|
||||||
Share anonymized recipe ratings to help improve suggestions
|
|
||||||
</label>
|
|
||||||
<p class="text-xs text-muted mt-xs">
|
|
||||||
When enabled, Kiwi sends the recipe source ID, your star rating, and
|
|
||||||
style tags to CircuitForge. No personal information or pantry contents
|
|
||||||
are included.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Display Preferences -->
|
<!-- Display Preferences -->
|
||||||
<section class="mt-md">
|
<section class="mt-md">
|
||||||
<h3 class="text-lg font-semibold mb-xs">Display</h3>
|
<h3 class="text-lg font-semibold mb-xs">Display</h3>
|
||||||
|
|
@ -399,7 +381,7 @@
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { useSettingsStore } from '../stores/settings'
|
import { useSettingsStore } from '../stores/settings'
|
||||||
import { useRecipesStore } from '../stores/recipes'
|
import { useRecipesStore } from '../stores/recipes'
|
||||||
import { householdAPI, settingsAPI, type HouseholdStatus } from '../services/api'
|
import { householdAPI, type HouseholdStatus } from '../services/api'
|
||||||
import type { TextureTag, SmellLevel, NoiseLevel } from '../services/api'
|
import type { TextureTag, SmellLevel, NoiseLevel } from '../services/api'
|
||||||
import type { TimeFirstLayout } from '../stores/settings'
|
import type { TimeFirstLayout } from '../stores/settings'
|
||||||
import { useOrchUsage } from '../composables/useOrchUsage'
|
import { useOrchUsage } from '../composables/useOrchUsage'
|
||||||
|
|
@ -408,23 +390,6 @@ const settingsStore = useSettingsStore()
|
||||||
const recipesStore = useRecipesStore()
|
const recipesStore = useRecipesStore()
|
||||||
const { enabled: orchPillEnabled, setEnabled: setOrchPillEnabled } = useOrchUsage()
|
const { enabled: orchPillEnabled, setEnabled: setOrchPillEnabled } = useOrchUsage()
|
||||||
|
|
||||||
// Cloud mode — baked in at build time via VITE_CLOUD_MODE=true in cloud builds
|
|
||||||
const isCloudMode = import.meta.env.VITE_CLOUD_MODE === 'true'
|
|
||||||
|
|
||||||
// Data sharing — magpie opt-in (cloud mode only)
|
|
||||||
const magpieOptIn = ref(false)
|
|
||||||
|
|
||||||
async function loadMagpieOptIn(): Promise<void> {
|
|
||||||
if (!isCloudMode) return
|
|
||||||
const value = await settingsAPI.getSetting('magpie_opt_in')
|
|
||||||
magpieOptIn.value = value === 'true'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setMagpieOptIn(enabled: boolean): Promise<void> {
|
|
||||||
magpieOptIn.value = enabled
|
|
||||||
await settingsAPI.setSetting('magpie_opt_in', enabled ? 'true' : 'false')
|
|
||||||
}
|
|
||||||
|
|
||||||
const timeFirstLayoutOptions: Array<{ value: TimeFirstLayout; label: string; description: string }> = [
|
const timeFirstLayoutOptions: Array<{ value: TimeFirstLayout; label: string; description: string }> = [
|
||||||
{ value: 'auto', label: 'Auto', description: 'Shows a time selector when recipes are available.' },
|
{ value: 'auto', label: 'Auto', description: 'Shows a time selector when recipes are available.' },
|
||||||
{ value: 'time_first', label: 'Time First', description: 'Always show the time bucket selector at the top.' },
|
{ value: 'time_first', label: 'Time First', description: 'Always show the time bucket selector at the top.' },
|
||||||
|
|
@ -574,7 +539,6 @@ async function handleRemoveMember(userId: string) {
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await settingsStore.load()
|
await settingsStore.load()
|
||||||
await loadHouseholdStatus()
|
await loadHouseholdStatus()
|
||||||
await loadMagpieOptIn()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── Sensory taxonomy ───────────────────────────────────────────────────────
|
// ── Sensory taxonomy ───────────────────────────────────────────────────────
|
||||||
|
|
@ -798,15 +762,13 @@ function getNoiseClass(_value: NoiseLevel, idx: number): string {
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.orch-pill-toggle,
|
.orch-pill-toggle {
|
||||||
.data-sharing-toggle {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.orch-pill-toggle input[type="checkbox"],
|
.orch-pill-toggle input[type="checkbox"] {
|
||||||
.data-sharing-toggle input[type="checkbox"] {
|
|
||||||
accent-color: var(--color-primary);
|
accent-color: var(--color-primary);
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue