From b2a1d292da4249e26cee3b9b50312654675f2cb1 Mon Sep 17 00:00:00 2001 From: robin Date: Fri, 19 Dec 2025 12:58:44 +0100 Subject: [PATCH] feat: implement advanced tagging system, tag weighting, and app focus refactoring - Implemented reusable TagSelector component with i18n support - Added tag weighting system (popularity scores 1-5) - Created admin panel for tag management - Integrated Nebius AI and Brave Search for 'Magic Scan' - Refactored app focus: removed bottle status, updated counters, and displayed extended bottle details - Updated i18n for German and English - Added database migration scripts --- .aiinstruct2 | 686 ++++++++++++++++++++++++++ .nebiusbravetasks | 114 +++++ .tagidea | 117 +++++ .whiskytagweight | 110 +++++ add_tag_weights_incremental.sql | 17 + global_products.sql | 29 ++ package.json | 1 + pnpm-lock.yaml | 36 +- src/app/admin/page.tsx | 6 + src/app/admin/tags/page.tsx | 196 ++++++++ src/app/bottles/[id]/page.tsx | 78 ++- src/components/BottleGrid.tsx | 42 +- src/components/CameraCapture.tsx | 53 +- src/components/StatsDashboard.tsx | 5 +- src/components/StatusSwitcher.tsx | 72 --- src/components/TagSelector.tsx | 151 ++++++ src/components/TastingList.tsx | 29 +- src/components/TastingNoteForm.tsx | 114 +++-- src/i18n/de.ts | 87 ++++ src/i18n/en.ts | 87 ++++ src/i18n/types.ts | 1 + src/lib/ai-client.ts | 6 + src/lib/supabase-admin.ts | 19 + src/services/analyze-bottle-nebius.ts | 124 +++++ src/services/brave-search.ts | 60 +++ src/services/magic-scan.ts | 87 ++++ src/services/save-tasting.ts | 25 +- src/services/tags.ts | 79 +++ src/types/whisky.ts | 22 +- tagging_system_migration.sql | 161 ++++++ 30 files changed, 2420 insertions(+), 194 deletions(-) create mode 100644 .aiinstruct2 create mode 100644 .nebiusbravetasks create mode 100644 .tagidea create mode 100644 .whiskytagweight create mode 100644 add_tag_weights_incremental.sql create mode 100644 global_products.sql create mode 100644 src/app/admin/tags/page.tsx delete mode 100644 src/components/StatusSwitcher.tsx create mode 100644 src/components/TagSelector.tsx create mode 100644 src/lib/ai-client.ts create mode 100644 src/lib/supabase-admin.ts create mode 100644 src/services/analyze-bottle-nebius.ts create mode 100644 src/services/brave-search.ts create mode 100644 src/services/magic-scan.ts create mode 100644 src/services/tags.ts create mode 100644 tagging_system_migration.sql diff --git a/.aiinstruct2 b/.aiinstruct2 new file mode 100644 index 0000000..83fc523 --- /dev/null +++ b/.aiinstruct2 @@ -0,0 +1,686 @@ +[ + { + "name": "auth_rls_initplan", + "title": "Auth RLS Initialization Plan", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if calls to `auth.()` in RLS policies are being unnecessarily re-evaluated for each row", + "detail": "Table `public.buddies` has a row level security policy `Manage own buddies` that re-evaluates an auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing `auth.()` with `(select auth.())`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "auth_rls_init_plan_public_buddies_Manage own buddies" + }, + { + "name": "auth_rls_initplan", + "title": "Auth RLS Initialization Plan", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if calls to `auth.()` in RLS policies are being unnecessarily re-evaluated for each row", + "detail": "Table `public.tasting_sessions` has a row level security policy `Manage own sessions` that re-evaluates an auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing `auth.()` with `(select auth.())`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "auth_rls_init_plan_public_tasting_sessions_Manage own sessions" + }, + { + "name": "auth_rls_initplan", + "title": "Auth RLS Initialization Plan", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if calls to `auth.()` in RLS policies are being unnecessarily re-evaluated for each row", + "detail": "Table `public.tastings` has a row level security policy `tastings_owner_all` that re-evaluates an auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing `auth.()` with `(select auth.())`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan", + "metadata": "{\"name\":\"tastings\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "auth_rls_init_plan_public_tastings_tastings_owner_all" + }, + { + "name": "auth_rls_initplan", + "title": "Auth RLS Initialization Plan", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if calls to `auth.()` in RLS policies are being unnecessarily re-evaluated for each row", + "detail": "Table `public.tasting_tags` has a row level security policy `tags_owner_all` that re-evaluates an auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing `auth.()` with `(select auth.())`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan", + "metadata": "{\"name\":\"tasting_tags\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "auth_rls_init_plan_public_tasting_tags_tags_owner_all" + }, + { + "name": "auth_rls_initplan", + "title": "Auth RLS Initialization Plan", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if calls to `auth.()` in RLS policies are being unnecessarily re-evaluated for each row", + "detail": "Table `public.session_participants` has a row level security policy `session_owner_all` that re-evaluates an auth.() for each row. This produces suboptimal query performance at scale. Resolve the issue by replacing `auth.()` with `(select auth.())`. See [docs](https://supabase.com/docs/guides/database/postgres/row-level-security#call-functions-with-select) for more info.", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0003_auth_rls_initplan", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "auth_rls_init_plan_public_session_participants_session_owner_all" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.bottles` has multiple permissive policies for role `authenticated` for action `DELETE`. Policies include `{\"Relaxed bottles access\",bottles_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"bottles\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_bottles_authenticated_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.bottles` has multiple permissive policies for role `authenticated` for action `INSERT`. Policies include `{\"Relaxed bottles access\",bottles_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"bottles\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_bottles_authenticated_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.bottles` has multiple permissive policies for role `authenticated` for action `SELECT`. Policies include `{\"Relaxed bottles access\",bottles_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"bottles\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_bottles_authenticated_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.bottles` has multiple permissive policies for role `authenticated` for action `UPDATE`. Policies include `{\"Relaxed bottles access\",bottles_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"bottles\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_bottles_authenticated_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `anon` for action `DELETE`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_anon_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `anon` for action `INSERT`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_anon_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `anon` for action `SELECT`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_anon_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `anon` for action `UPDATE`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_anon_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `authenticated` for action `DELETE`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_authenticated_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `authenticated` for action `INSERT`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_authenticated_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `authenticated` for action `SELECT`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_authenticated_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `authenticated` for action `UPDATE`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_authenticated_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `authenticator` for action `DELETE`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_authenticator_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `authenticator` for action `INSERT`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_authenticator_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `authenticator` for action `SELECT`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_authenticator_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `authenticator` for action `UPDATE`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_authenticator_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `dashboard_user` for action `DELETE`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_dashboard_user_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `dashboard_user` for action `INSERT`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_dashboard_user_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `dashboard_user` for action `SELECT`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_dashboard_user_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.buddies` has multiple permissive policies for role `dashboard_user` for action `UPDATE`. Policies include `{\"Manage own buddies\",buddies_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"buddies\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_buddies_dashboard_user_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `anon` for action `DELETE`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_anon_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `anon` for action `INSERT`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_anon_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `anon` for action `SELECT`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_anon_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `anon` for action `UPDATE`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_anon_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `authenticated` for action `DELETE`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_authenticated_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `authenticated` for action `INSERT`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_authenticated_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `authenticated` for action `SELECT`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_authenticated_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `authenticated` for action `UPDATE`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_authenticated_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `authenticator` for action `DELETE`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_authenticator_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `authenticator` for action `INSERT`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_authenticator_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `authenticator` for action `SELECT`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_authenticator_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `authenticator` for action `UPDATE`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_authenticator_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `dashboard_user` for action `DELETE`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_dashboard_user_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `dashboard_user` for action `INSERT`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_dashboard_user_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `dashboard_user` for action `SELECT`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_dashboard_user_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.session_participants` has multiple permissive policies for role `dashboard_user` for action `UPDATE`. Policies include `{session_owner_all,session_participants_owner_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"session_participants\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_session_participants_dashboard_user_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `anon` for action `DELETE`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_anon_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `anon` for action `INSERT`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_anon_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `anon` for action `SELECT`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_anon_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `anon` for action `UPDATE`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_anon_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `authenticated` for action `DELETE`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_authenticated_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `authenticated` for action `INSERT`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_authenticated_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `authenticated` for action `SELECT`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_authenticated_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `authenticated` for action `UPDATE`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_authenticated_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `authenticator` for action `DELETE`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_authenticator_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `authenticator` for action `INSERT`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_authenticator_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `authenticator` for action `SELECT`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_authenticator_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `authenticator` for action `UPDATE`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_authenticator_UPDATE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `dashboard_user` for action `DELETE`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_dashboard_user_DELETE" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `dashboard_user` for action `INSERT`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_dashboard_user_INSERT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `dashboard_user` for action `SELECT`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_dashboard_user_SELECT" + }, + { + "name": "multiple_permissive_policies", + "title": "Multiple Permissive Policies", + "level": "WARN", + "facing": "EXTERNAL", + "categories": "[\"PERFORMANCE\"]", + "description": "Detects if multiple permissive row level security policies are present on a table for the same `role` and `action` (e.g. insert). Multiple permissive policies are suboptimal for performance as each policy must be executed for every relevant query.", + "detail": "Table `public.tasting_sessions` has multiple permissive policies for role `dashboard_user` for action `UPDATE`. Policies include `{\"Manage own sessions\",session_access_policy}`", + "remediation": "https://supabase.com/docs/guides/database/database-linter?lint=0006_multiple_permissive_policies", + "metadata": "{\"name\":\"tasting_sessions\",\"type\":\"table\",\"schema\":\"public\"}", + "cache_key": "multiple_permissive_policies_public_tasting_sessions_dashboard_user_UPDATE" + } +] \ No newline at end of file diff --git a/.nebiusbravetasks b/.nebiusbravetasks new file mode 100644 index 0000000..8203c55 --- /dev/null +++ b/.nebiusbravetasks @@ -0,0 +1,114 @@ +# IMPLEMENTIERUNGS-PLAN: Whisky Scanner App +## Stack: Next.js | Supabase | Nebius AI | Brave Search | Coolify + +--- + +### 1. SICHERHEIT & INFRASTRUKTUR (Fundament) + +**Ziel:** Sicherstellen, dass keine API-Keys leaken und keine User-Daten direkt an US-Anbieter fließen. + +- [ ] **Environment Variablen säubern** + - Prüfe deine `.env` Datei. + - [ ] `NEBIUS_API_KEY`: Darf NICHT mit `NEXT_PUBLIC_` beginnen. + - [ ] `BRAVE_API_KEY`: Darf NICHT mit `NEXT_PUBLIC_` beginnen. + - [ ] `SUPABASE_SERVICE_ROLE_KEY`: Darf NICHT mit `NEXT_PUBLIC_` beginnen (für Admin-Schreibrechte). + - [ ] `NEXT_PUBLIC_SUPABASE_ANON_KEY`: Darf öffentlich sein (nur für RLS Lesezugriffe). + +- [ ] **Server-Side Architektur erzwingen** + - Stelle sicher, dass alle Aufrufe zu Nebius und Brave ausschließlich über **Server Actions** (`'use server'`) oder **API Routes** (`app/api/...`) laufen. + - *Test:* Öffne die Entwicklertools im Browser (Netzwerk-Tab). Beim Scannen darf KEIN Aufruf zu `api.brave.com` oder `api.studio.nebius.ai` sichtbar sein. Nur Aufrufe zu deinem eigenen Backend (localhost/deine-domain). + +- [ ] **Supabase Admin Client einrichten** + - Erstelle eine Utility-Datei (z.B. `lib/supabase-admin.ts`), die `createClient` mit dem `SUPABASE_SERVICE_ROLE_KEY` initialisiert. + - Nutze diesen Client NUR in Server Actions für das Schreiben in den globalen Cache. + +--- + +### 2. NEBIUS AI STUDIO (Das "Auge") + +**Ziel:** Bildanalyse und Extraktion strukturierter Daten für ca. 0,03 Cent pro Bild. + +- [ ] **OpenAI SDK konfigurieren** + - Installiere das SDK: `npm install openai` + - Initialisiere den Client in `lib/ai-client.ts`: + ```typescript + import { OpenAI } from "openai"; + export const aiClient = new OpenAI({ + baseURL: "[https://api.studio.nebius.ai/v1](https://api.studio.nebius.ai/v1)", + apiKey: process.env.NEBIUS_API_KEY, + }); + ``` + +- [ ] **Prompt Engineering (JSON Mode)** + - Erstelle den Prompt für das Modell `Qwen/Qwen2.5-VL-72B-Instruct` (oder Qwen2-VL-72B). + - Anweisung: "Extrahiere: Distillery, Name/Edition, Age/Vintage, ABV. Formatiere als reines JSON." + - WICHTIG: Setze `response_format: { type: "json_object" }` im API-Call, damit du valides JSON erhältst. + +- [ ] **Such-Query Generierung** + - Lass die AI zusätzlich ein Feld `search_string` generieren. + - Format-Vorgabe an AI: `"site:whiskybase.com [Distillery] [Name] [Vintage]"` + - *Grund:* Das spart Logik im Code, die AI weiß am besten, was auf dem Etikett wichtig ist. + +Bitte für den Adminuser einen Schalter zwischen Gemini und Nebius implementieren. Idealerweise im Magic Scan Formular. +--- + +### 3. BRAVE SEARCH API (Der "Notfall-Plan") + +**Ziel:** Finden der Whiskybase-ID, wenn der Cache leer ist (Prepaid, keine Fixkosten). + +- [ ] **Fetch-Funktion bauen** + - Erstelle eine Server-Funktion, die Brave aufruft. + - URL: `https://api.search.brave.com/res/v1/web/search?q=...` + - Header: `X-Subscription-Token: process.env.BRAVE_API_KEY` + +- [ ] **Ergebnis-Parser implementieren** + - Nimm das erste Ergebnis (`results[0]`). + - Prüfe per Regex, ob der Link valide ist: `whiskybase.com/whiskies/whisky/([0-9]+)/...` + - Extrahiere die ID (z.B. "12345"). + +- [ ] **Fallback-Logik** + - Wenn Brave nichts findet (array leer), gib einen Fehler zurück oder markiere das Produkt als "Manuelle Prüfung nötig". + +--- + +### 4. DATENBANK & CACHING (Die "Spar-Maschine") + +**Ziel:** Suchkosten minimieren und Datenqualität sichern. + +- [ ] **Datenbank-Schema (Tabelle `global_products`)** + - `id`: uuid (PK) + - `wb_id`: text (Whiskybase ID, unique) + - `full_name`: text (z.B. "Lagavulin 16") + - `search_vector`: tsvector (Für unscharfe Suche!) + - `image_hash`: text (Optional, falls du Hash-Vergleiche machst) + - `created_at`: timestamp + +- [ ] **Row Level Security (RLS) Policies** + - Tabelle `global_products`: + - Policy "Enable Read Access for all users": `true` (für SELECT). + - Policy "Disable Insert/Update for users": `false` (für INSERT/UPDATE). + - *Grund:* Nur dein Server (Service Role) darf hier schreiben, um "Cache Poisoning" durch User zu verhindern. + +- [ ] **Der "Main Loop" (Server Action)** + 1. **AI:** Bild an Nebius -> JSON erhalten. + 2. **DB Check:** Suche in `global_products` mit `.textSearch('search_vector', ai_name)`. + 3. **Entscheidung:** + - *Treffer:* Nimm ID aus DB (Kosten: 0€). Return an Client. + - *Kein Treffer:* Rufe Brave Search auf (Kosten: 0,3 Cent). + 4. **Cache Write:** Wenn Brave erfolgreich war, nutze `supabaseAdmin`, um den neuen Whisky in `global_products` zu speichern. + +--- + +### 5. DSGVO / PRIVACY CHECKLISTE + +**Ziel:** Rechtssicherer Betrieb in der EU. + +- [ ] **Data Minimization** + - Schicke an Brave NUR den Suchstring (z.B. "Lagavulin 16"). + - Schicke NIEMALS User-IDs, E-Mails oder IP-Adressen im Header oder Body an Brave. + +- [ ] **Proxy-Check** + - Bestätige nochmals, dass Brave nur die IP deines Hetzner-Servers sieht, nie die des Endnutzers (siehe Punkt 1). + +- [ ] **Nebius Region** + - Prüfe im Nebius Dashboard, dass dein Projekt in der Region **eu-north1** (Finnland) oder einer anderen EU-Region läuft, falls wählbar. \ No newline at end of file diff --git a/.tagidea b/.tagidea new file mode 100644 index 0000000..1042f11 --- /dev/null +++ b/.tagidea @@ -0,0 +1,117 @@ + +-------------------------------------------------------------------------------- +2. ERWEITERTES TAGGING-SYSTEM (NOSE, TASTE, FINISH) +-------------------------------------------------------------------------------- +Das Ziel ist es, Textwüsten zu vermeiden und schnelles Erfassen zu ermöglichen. Das muss möglichst mobile friendly und easy für den Nutzer sein! + +DATENBANK-LOGIK (SUPABASE EMPFEHLUNG): +Tabelle `tags`: +- id (uuid) +- name (string) +- category (enum: 'nose', 'taste', 'finish', 'texture') +- is_system_default (boolean) -> True = Admin Tag, False = User Custom Tag +- created_by (uuid, nullable) -> User ID, falls Custom Tag + +FUNKTIONSWEISE: +1. Standard-Tags: Beim Erstellen einer Notiz werden die System-Tags als Chips vorgeschlagen (durchsuchbar). Bitte denke an i18n und übersetze die System-Tags bereits. +2. Custom Tags: Findet der User ein Aroma nicht, tippt er es ein -> Es wird als "User Tag" nur für ihn gespeichert. +3. Vorauswahl durch AI: Gemini könnte beim Scan basierend auf der Whisky-Datenbank schon Tags "pre-selecten" (highlighten). + +-------------------------------------------------------------------------------- +3. DIE "PROFUNDE" TAG-LISTE (MASTER LIST FÜR ADMIN PANEL) +-------------------------------------------------------------------------------- +Diese Liste deckt das Spektrum von milden Lowlands bis zu schweren Islay Whiskys ab. + +KATEGORIE A: FRUCHTIG (FRUITY) +- Apfel (Grüner Apfel / Bratapfel) +- Birne +- Zitrone / Zitrus +- Orange / Orangenschale +- Pfirsich / Aprikose +- Banane +- Ananas / Tropische Früchte +- Kirsche +- Beeren (Brombeere / Himbeere) +- Pflaume +- Trockenfrüchte (Rosinen / Datteln / Feigen) -> Typisch für Sherry-Fässer! + +KATEGORIE B: SÜSS & CREMIG (SWEET & CREAMY) +- Vanille +- Honig +- Karamell / Toffee +- Schokolade (Zartbitter / Milch) +- Malz / Müsli +- Butter / Butterkeks +- Marzipan / Mandel +- Sahnebonbon + +KATEGORIE C: WÜRZIG & NUSSIG (SPICY & NUTTY) +- Eiche (frisch / alt) +- Zimt +- Pfeffer (schwarz / weiß) +- Muskatnuss +- Ingwer +- Nelke +- Walnuss +- Haselnuss +- Geröstete Nüsse + +KATEGORIE D: RAUCHIG & TORFIG (PEATY & SMOKY) +- Lagerfeuer / Holzkohle +- Torfrauch +- Asche +- Medizinisch (Jod / Verbandszeug) -> Typisch Laphroaig +- Teer / Asphalt +- Geräucherter Schinken / Speck +- Grillfleisch + +KATEGORIE E: MARITIM & SALZIG (COASTAL) +- Meersalz / Salzlake +- Seetang / Algen +- Austern +- Frische Meeresbrise + +KATEGORIE F: FLORAL & KRÄUTER (FLORAL & HERBAL) +- Heidekraut (Heather) +- Gras / Heu (frisch gemäht) +- Minze / Menthol +- Eukalyptus +- Tabak (frisch / Pfeife) +- Leder +- Tee (Schwarztee / Grüntee) + +KATEGORIE G: FEHLNOTEN / SPEZIELLES (OFF-NOTES / SPECIAL) +- Schwefel (Streichholz) +- Gummi +- Seife (Lavendel) +- Klebstoff (Uhu) -> oft bei jungen Grain Whiskys +- Pilze / Waldboden (erdig) + +-------------------------------------------------------------------------------- +4. DAS FINISH (ABGANG) - SPEZIAL TAGS +-------------------------------------------------------------------------------- +Beim Abgang geht es oft weniger um neuen Geschmack, sondern um Gefühl und Dauer. + +DAUER: +- Kurz & Knackig +- Mittellang +- Lang anhaltend +- Ewig + +TEXTUR & GEFÜHL (MOUTHFEEL): +- Ölig / Viskos +- Trocken (Adstringierend - zieht den Mund zusammen) +- Wärmend (Alkoholisch) +- Scharf / Beißend +- Weich / Samtig +- Wässrig + +-------------------------------------------------------------------------------- +5. ADMIN PANEL IDEEN +-------------------------------------------------------------------------------- +Um diese Tags zu verwalten, brauchst du im Admin-Bereich (`/admin/tags`) folgende Features: + +1. Bulk Import: Ein Script oder Textfeld, um obige Liste einmalig reinzuladen. +2. Tag Merging: Wenn 5 User "Apfelkuchen" als Custom Tag anlegen, kannst du entscheiden: "Okay, das wird ein offizieller System-Tag". +3. Farb-Codierung: Weise den Kategorien Farben zu (z.B. Frucht = Grün/Rot, Rauch = Grau, Süß = Gelb). Das macht die UI im Frontend viel schöner. +4. Synonym-Matching: (Advanced) Wenn User "Smoke" tippt, schlage "Rauch" vor. \ No newline at end of file diff --git a/.whiskytagweight b/.whiskytagweight new file mode 100644 index 0000000..b58d327 --- /dev/null +++ b/.whiskytagweight @@ -0,0 +1,110 @@ +Ich habe die Liste für dich überarbeitet und gewichtet. Wir gehen hier am besten zweigleisig vor: + + Allgemeine Wahrscheinlichkeit: Wie oft kommt eine Note generell bei Whisky vor? (Vanille = sehr oft, Lavendel = sehr selten). + + User-spezifische Sortierung (Tech-Tipp): In deiner Supabase-Query solltest du später die Tags nach usage_count (wie oft hat dieser User den Tag schon benutzt) sortieren. + +Hier ist die aktualisierte Liste als Textdatei-Inhalt. Ich habe einen "Popularity Score" (1-5) hinzugefügt. + + 5 = Fast immer dabei (Standard-Aromen) -> Diese müssen ganz oben stehen. + + 1 = Exoten / Fehlnoten -> Diese können weiter unten in der Liste auftauchen. + +Hier ist der Datei-Inhalt für deinen Import: + +# WHISKY TAGS - GEWICHTET NACH HÄUFIGKEIT (1-5) +# 5 = Sehr häufig (Top Level) +# 1 = Sehr selten (Nische) + +-------------------------------------------------------------------------------- +KATEGORIE: SÜSS & FASS (SWEET & CASK) +-------------------------------------------------------------------------------- +(5) Vanille # Der Klassiker (Ex-Bourbon Fässer) +(5) Eiche (würzig) # Basis fast aller Whiskys +(5) Karamell / Toffee # Durch Fassausbrennung +(4) Honig # Sehr häufig bei Speyside/Highland +(4) Malz / Getreide # Der Rohstoff selbst +(3) Schokolade (Dunkel) # Oft bei Sherry-Fässern +(3) Nüsse (Allgemein) # Oxidative Note +(2) Marzipan # Eher spezifisch +(2) Butterkeks # Typisch für junge Malts +(1) Kokosnuss # Speziell (oft bei alter US-Eiche) + +-------------------------------------------------------------------------------- +KATEGORIE: FRUCHT (FRUITY) +-------------------------------------------------------------------------------- +(5) Zitrus / Zitrone # Frische Kopfnote +(4) Apfel (Grün/Rot) # Standard Ester-Note +(4) Birne # Typisch für Glenfiddich etc. +(4) Trockenfrüchte # Rosinen/Sultaninen (Sherry-Einfluss) +(3) Orange / Schale # Häufig +(3) Beeren (Dunkel) # Brombeere/Johannisbeere +(2) Pfirsich / Aprikose # Steinobst +(2) Banane # Spezielle Hefe-Note (z.B. Jack Daniels) +(2) Pflaume # Reife Note +(1) Ananas / Tropisch # Eher bei alten Malts / Iren +(1) Melone # Selten + +-------------------------------------------------------------------------------- +KATEGORIE: WÜRZE & KRÄUTER (SPICE & HERBAL) +-------------------------------------------------------------------------------- +(4) Pfeffer (Schwarz) # Der "Biss" (Talisker) +(4) Zimt # Weihnachtsgewürze (Sherry/Virgin Oak) +(3) Ingwer # Schärfe & Frische +(3) Gras / Heu # Lowland Stil / Junger Whisky +(2) Muskatnuss # Holzwürze +(2) Minze / Menthol # Oft im Finish +(2) Nelke # Intensiv würzig +(1) Eukalyptus # Sehr frisch +(1) Anis / Lakritz # Speziell + +-------------------------------------------------------------------------------- +KATEGORIE: RAUCH & MARITIM (SMOKE & COASTAL) +-------------------------------------------------------------------------------- +# Achtung: Diese sind bei Islay-Fans eine (5), global gesehen eher eine (3). +# Ich gewichte sie hier hoch, da "Kenner" sie oft suchen. + +(4) Torfrauch # Der Klassiker +(4) Lagerfeuer # Holzrauch +(3) Meersalz / Brine # Küsten-Whiskys +(3) Asche # Trockener Rauch +(3) Speck / Schinken # Fleischiger Rauch +(2) Medizinisch / Jod # Laphroaig-Style +(2) Teer # Schwerer Rauch +(1) Seetang / Alge # Sehr speziell +(1) Gummistiefel # Industrie-Note + +-------------------------------------------------------------------------------- +KATEGORIE: TEXTUR & FINISH (FEELING) +-------------------------------------------------------------------------------- +(5) Wärmend # Alkohol-Wirkung +(4) Trocken # Adstringierend (Eiche) +(4) Lang anhaltend # Qualitätsmerkmal +(3) Ölig / Cremig # Mundgefühl +(3) Würzig # Prickeln +(2) Kurz # Eher negativ / leicht +(1) Wässrig # Negativ + +-------------------------------------------------------------------------------- +KATEGORIE: SPEZIELLES / OFF-NOTES +-------------------------------------------------------------------------------- +(2) Leder # Alter Whisky / Sherry +(2) Tabak # Alter Whisky +(2) Schwefel # Kann Fehlrnote sein, oder gewollt "dreckig" +(1) Pilze / Erdig # Dunnage Warehouse Funk +(1) Seife # Oft als Fehler wahrgenommen +(1) Metallisch # Fehler bei der Destillation + +Tipp zur Implementierung der "Smart Sorting" Logik + +Damit der User sofort das Richtige findet, würde ich im Frontend (oder in der API) eine kombinierte Sortierung bauen. + +Stell dir vor, Gemini hat erkannt: "Das ist ein Ardbeg 10". Dann weiß dein System (via Gemini oder Datenbank): Kategorie = Islay / Rauchig. + +Deine Sortierlogik für die Tags sollte dann so aussehen: + + Priorität A (Der "AI-Kontext"): Wenn Gemini sagt "Likely Peated", dann booste alle Tags aus der Kategorie "RAUCH" temporär nach ganz oben. User sieht sofort: Torf, Rauch, Asche. + + Priorität B (Global Weight): Danach kommen die Tags mit Score 5 und 4 (Vanille, Eiche, Apfel). User sieht die Standards. + + Priorität C (Der Rest): Ganz unten kommen die Exoten (Ananas, Seife). \ No newline at end of file diff --git a/add_tag_weights_incremental.sql b/add_tag_weights_incremental.sql new file mode 100644 index 0000000..b9e8877 --- /dev/null +++ b/add_tag_weights_incremental.sql @@ -0,0 +1,17 @@ +-- Incremental Migration for Tag Popularity Scores +ALTER TABLE tags ADD COLUMN IF NOT EXISTS popularity_score INTEGER DEFAULT 3; + +-- Update scores for common aromas (5) +UPDATE tags SET popularity_score = 5 WHERE name IN ('Vanille', 'Eiche', 'Karamell', 'Zitrone', 'Zitrus', 'Wärmend'); + +-- Update scores for frequent aromas (4) +UPDATE tags SET popularity_score = 4 WHERE name IN ('Honig', 'Malz', 'Apfel', 'Grüner Apfel', 'Bratapfel', 'Birne', 'Trockenfrüchte', 'Pfeffer', 'Zimt', 'Torfrauch', 'Lagerfeuer', 'Trocken', 'Lang anhaltend'); + +-- Update scores for regular aromas (3) +UPDATE tags SET popularity_score = 3 WHERE name IN ('Schokolade', 'Zartbitterschokolade', 'Milchschokolade', 'Nüsse', 'Orange', 'Orangenschale', 'Beeren', 'Brombeere', 'Himbeere', 'Ingwer', 'Gras', 'Heu', 'Meersalz', 'Salzlake', 'Asche', 'Speck', 'Geräucherter Schinken', 'Grillfleisch', 'Ölig', 'Viskos', 'Würzig'); + +-- Update scores for specific aromas (2) +UPDATE tags SET popularity_score = 2 WHERE name IN ('Marzipan', 'Butterkeks', 'Pfirsich', 'Aprikose', 'Banane', 'Pflaume', 'Muskatnuss', 'Minze', 'Menthol', 'Nelke', 'Medizinisch', 'Jod', 'Teer', 'Asphalt', 'Leder', 'Tabak', 'Schwefel', 'Kurz'); + +-- Update scores for rare/exotic aromas (1) +UPDATE tags SET popularity_score = 1 WHERE name IN ('Kokosnuss', 'Ananas', 'Tropische Früchte', 'Melone', 'Eukalyptus', 'Seetang', 'Algen', 'Austern', 'Wässrig', 'Pilze', 'Seife', 'Metallisch'); diff --git a/global_products.sql b/global_products.sql new file mode 100644 index 0000000..969064a --- /dev/null +++ b/global_products.sql @@ -0,0 +1,29 @@ +-- Global Products for caching searches +CREATE TABLE IF NOT EXISTS global_products ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + wb_id TEXT UNIQUE NOT NULL, + full_name TEXT NOT NULL, + search_vector tsvector GENERATED ALWAYS AS (to_tsvector('simple', full_name)) STORED, + image_hash TEXT, + created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) +); + +-- Index for search vector +CREATE INDEX IF NOT EXISTS idx_global_products_search_vector ON global_products USING GIN (search_vector); + +-- Enable RLS +ALTER TABLE global_products ENABLE ROW LEVEL SECURITY; + +-- Policies for global_products +-- Enable Read Access for all users (SELECT) +DROP POLICY IF EXISTS "Enable Read Access for all users" ON global_products; +CREATE POLICY "Enable Read Access for all users" +ON global_products FOR SELECT +USING (true); + +-- Disable Insert/Update for normal users (only Service Role/Admins) +DROP POLICY IF EXISTS "Enable Admin Insert/Update" ON global_products; +CREATE POLICY "Enable Admin Insert/Update" +ON global_products FOR ALL +USING (EXISTS (SELECT 1 FROM admin_users WHERE user_id = (SELECT auth.uid()))) +WITH CHECK (EXISTS (SELECT 1 FROM admin_users WHERE user_id = (SELECT auth.uid()))); diff --git a/package.json b/package.json index 8dc89ec..8249dec 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "heic2any": "^0.0.4", "lucide-react": "^0.300.0", "next": "14.2.23", + "openai": "^6.15.0", "react": "^18", "react-dom": "^18", "sharp": "^0.34.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3c698b..c954e07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: next: specifier: 14.2.23 version: 14.2.23(@babel/core@7.28.5)(@playwright/test@1.57.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + openai: + specifier: ^6.15.0 + version: 6.15.0(ws@8.18.3)(zod@3.25.76) react: specifier: ^18 version: 18.3.1 @@ -2233,6 +2236,18 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + openai@6.15.0: + resolution: {integrity: sha512-F1Lvs5BoVvmZtzkUEVyh8mDQPPFolq4F+xdsx/DO8Hee8YF3IGAlZqUIsF+DVGhqf4aU0a3bTghsxB6OIsRy1g==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -4357,8 +4372,8 @@ snapshots: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) @@ -4377,7 +4392,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -4388,22 +4403,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -4414,7 +4429,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -5152,6 +5167,11 @@ snapshots: dependencies: wrappy: 1.0.2 + openai@6.15.0(ws@8.18.3)(zod@3.25.76): + optionalDependencies: + ws: 8.18.3 + zod: 3.25.76 + optionator@0.9.4: dependencies: deep-is: 0.1.4 diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 97bc406..379d383 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -100,6 +100,12 @@ export default async function AdminPage() { > Manage Plans + + Manage Tags + ([]); + const [isLoading, setIsLoading] = useState(true); + const [search, setSearch] = useState(''); + const [categoryFilter, setCategoryFilter] = useState('all'); + + useEffect(() => { + fetchTags(); + }, []); + + const fetchTags = async () => { + setIsLoading(true); + const { data, error } = await supabase + .from('tags') + .select('*') + .order('popularity_score', { ascending: false }) + .order('name'); + + if (data) setTags(data); + setIsLoading(false); + }; + + const filteredTags = tags.filter(tag => { + const matchesSearch = tag.name.toLowerCase().includes(search.toLowerCase()); + const matchesCategory = categoryFilter === 'all' || tag.category === categoryFilter; + return matchesSearch && matchesCategory; + }); + + const handleDelete = async (id: string) => { + if (!confirm('Tag wirklich löschen?')) return; + const { error } = await supabase.from('tags').delete().eq('id', id); + if (!error) setTags(prev => prev.filter(t => t.id !== id)); + }; + + const toggleSystemDefault = async (tag: Tag) => { + const { error } = await supabase + .from('tags') + .update({ is_system_default: !tag.is_system_default }) + .eq('id', tag.id); + + if (!error) { + setTags(prev => prev.map(t => t.id === tag.id ? { ...t, is_system_default: !t.is_system_default } : t)); + } + }; + + const updatePopularity = async (tagId: string, score: number) => { + const { error } = await supabase + .from('tags') + .update({ popularity_score: score }) + .eq('id', tagId); + + if (!error) { + setTags(prev => prev.map(t => t.id === tagId ? { ...t, popularity_score: score } : t)); + } + }; + + return ( +
+
+
+
+ + Admin Dashboard + +

+ Aroma Tags verwalten +

+
+
+ +
+
+
+ + setSearch(e.target.value)} + placeholder="Tags suchen..." + className="w-full pl-10 pr-4 py-2 bg-white dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all dark:text-zinc-200" + /> +
+
+ + +
+
+ +
+ + + + + + + + + + + + {isLoading ? ( + + + + ) : filteredTags.length === 0 ? ( + + + + ) : ( + filteredTags.map((tag) => ( + + + + + + + + )) + )} + +
NameKategoriePopularitätTypAktionen
Lade Tags...
Keine Tags gefunden.
+
+ {tag.is_system_default ? t(`aroma.${tag.name}`) : tag.name} +
+ {tag.is_system_default && ( +
{tag.name} (Key)
+ )} +
+ + {tag.category} + + +
+ {[1, 2, 3, 4, 5].map((score) => ( + + ))} +
+
+ + + +
+
+
+
+
+ ); +} diff --git a/src/app/bottles/[id]/page.tsx b/src/app/bottles/[id]/page.tsx index 49e6d40..42ef34b 100644 --- a/src/app/bottles/[id]/page.tsx +++ b/src/app/bottles/[id]/page.tsx @@ -2,10 +2,9 @@ import { createServerComponentClient } from '@supabase/auth-helpers-nextjs'; import { cookies } from 'next/headers'; import { notFound } from 'next/navigation'; import Link from 'next/link'; -import { ChevronLeft, Calendar, Award, Droplets, MapPin, Tag, ExternalLink, Package, PlusCircle } from 'lucide-react'; +import { ChevronLeft, Calendar, Award, Droplets, MapPin, Tag, ExternalLink, Package, PlusCircle, Info } from 'lucide-react'; import { getStorageUrl } from '@/lib/supabase'; import TastingNoteForm from '@/components/TastingNoteForm'; -import StatusSwitcher from '@/components/StatusSwitcher'; import TastingList from '@/components/TastingList'; import DeleteBottleButton from '@/components/DeleteBottleButton'; import EditBottleForm from '@/components/EditBottleForm'; @@ -48,11 +47,19 @@ export default async function BottlePage({ id, name ), - tasting_tags ( + tasting_buddies ( buddies ( id, name ) + ), + tasting_tags ( + tags ( + id, + name, + category, + is_system_default + ) ) `) .eq('bottle_id', params.id) @@ -68,9 +75,11 @@ export default async function BottlePage({ .select(` *, tasting_tags ( - buddies ( + tags ( id, - name + name, + category, + is_system_default ) ) `) @@ -125,32 +134,60 @@ export default async function BottlePage({ )} -
+
-
- Kategorie +
+ Kategorie
-
{bottle.category || '-'}
+
{bottle.category || '-'}
-
- Alkoholgehalt +
+ Alkoholgehalt
-
{bottle.abv}% Vol.
+
{bottle.abv}% Vol.
-
- Alter +
+ Alter
-
{bottle.age ? `${bottle.age} Jahre` : '-'}
+
{bottle.age ? `${bottle.age} J.` : '-'}
-
-
- Zuletzt verkostet + + {bottle.distilled_at && ( +
+
+ Destilliert +
+
{bottle.distilled_at}
-
+ )} + + {bottle.bottled_at && ( +
+
+ Abgefüllt +
+
{bottle.bottled_at}
+
+ )} + + {bottle.batch_info && ( +
+
+ Batch / Code +
+
{bottle.batch_info}
+
+ )} + +
+
+ Letzter Dram +
+
{tastings && tastings.length > 0 ? new Date(tastings[0].created_at).toLocaleDateString('de-DE') : 'Noch nie'} @@ -158,8 +195,7 @@ export default async function BottlePage({
-
- +
diff --git a/src/components/BottleGrid.tsx b/src/components/BottleGrid.tsx index ed121ce..29ceb44 100644 --- a/src/components/BottleGrid.tsx +++ b/src/components/BottleGrid.tsx @@ -2,7 +2,7 @@ import React, { useState, useMemo } from 'react'; import Link from 'next/link'; -import { Search, Filter, X, Calendar, Clock, Package, Lock, Unlock, Ghost, FlaskConical, AlertCircle, Trash2, AlertTriangle, PlusCircle } from 'lucide-react'; +import { Search, Filter, X, Calendar, Clock, Package, FlaskConical, AlertCircle, Trash2, AlertTriangle, PlusCircle } from 'lucide-react'; import { getStorageUrl } from '@/lib/supabase'; import { useSearchParams } from 'next/navigation'; import { validateSession } from '@/services/validate-session'; @@ -19,7 +19,6 @@ interface Bottle { age: number; image_url: string; purchase_price?: number | null; - status: 'sealed' | 'open' | 'sampled' | 'empty'; created_at: string; last_tasted?: string | null; is_whisky?: boolean; @@ -33,15 +32,6 @@ interface BottleCardProps { function BottleCard({ bottle, sessionId }: BottleCardProps) { const { t, locale } = useI18n(); - const statusConfig = { - open: { icon: Unlock, color: 'bg-amber-500/80 border-amber-400/50', label: t('bottle.status.open') }, - sampled: { icon: FlaskConical, color: 'bg-purple-500/80 border-purple-400/50', label: t('bottle.status.sampled') }, - empty: { icon: Ghost, color: 'bg-zinc-500/80 border-zinc-400/50', label: t('bottle.status.empty') }, - sealed: { icon: Lock, color: 'bg-blue-600/80 border-blue-400/50', label: t('bottle.status.sealed') }, - }; - - const StatusIcon = statusConfig[bottle.status as keyof typeof statusConfig]?.icon || Lock; - const statusStyle = statusConfig[bottle.status as keyof typeof statusConfig] || statusConfig.sealed; return ( )} -
- - {statusStyle.label} -
@@ -144,7 +130,6 @@ export default function BottleGrid({ bottles }: BottleGridProps) { const [searchQuery, setSearchQuery] = useState(''); const [selectedCategory, setSelectedCategory] = useState(null); const [selectedDistillery, setSelectedDistillery] = useState(null); - const [selectedStatus, setSelectedStatus] = useState(null); const [sortBy, setSortBy] = useState<'name' | 'last_tasted' | 'created_at'>('created_at'); const categories = useMemo(() => { @@ -174,9 +159,8 @@ export default function BottleGrid({ bottles }: BottleGridProps) { const matchesCategory = !selectedCategory || bottle.category === selectedCategory; const matchesDistillery = !selectedDistillery || bottle.distillery === selectedDistillery; - const matchesStatus = !selectedStatus || bottle.status === selectedStatus; - return matchesSearch && matchesCategory && matchesDistillery && matchesStatus; + return matchesSearch && matchesCategory && matchesDistillery; }); // Sorting logic @@ -191,7 +175,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) { return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); } }); - }, [bottles, searchQuery, selectedCategory, selectedDistillery, selectedStatus, sortBy]); + }, [bottles, searchQuery, selectedCategory, selectedDistillery, sortBy]); const [isFiltersOpen, setIsFiltersOpen] = useState(false); @@ -203,7 +187,7 @@ export default function BottleGrid({ bottles }: BottleGridProps) { ); } - const activeFiltersCount = (selectedCategory ? 1 : 0) + (selectedDistillery ? 1 : 0) + (selectedStatus ? 1 : 0); + const activeFiltersCount = (selectedCategory ? 1 : 0) + (selectedDistillery ? 1 : 0); return (
@@ -314,23 +298,6 @@ export default function BottleGrid({ bottles }: BottleGridProps) {
-
- -
- {['sealed', 'open', 'sampled', 'empty'].map((status) => ( - - ))} -
-
@@ -338,7 +305,6 @@ export default function BottleGrid({ bottles }: BottleGridProps) { onClick={() => { setSelectedCategory(null); setSelectedDistillery(null); - setSelectedStatus(null); setSearchQuery(''); }} className="text-[10px] font-black text-red-500 uppercase tracking-widest hover:underline" diff --git a/src/components/CameraCapture.tsx b/src/components/CameraCapture.tsx index e0918a1..ee4b70d 100644 --- a/src/components/CameraCapture.tsx +++ b/src/components/CameraCapture.tsx @@ -1,7 +1,8 @@ 'use client'; import React, { useRef, useState } from 'react'; -import { Camera, Upload, CheckCircle2, AlertCircle, X, Search, ExternalLink, ArrowRight, Loader2, Wand2, Plus, Sparkles, Droplets, ChevronRight } from 'lucide-react'; +import { Camera, Upload, CheckCircle2, AlertCircle, X, Search, ExternalLink, ArrowRight, Loader2, Wand2, Plus, Sparkles, Droplets, ChevronRight, User } from 'lucide-react'; + import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; import { useRouter, useSearchParams } from 'next/navigation'; import { analyzeBottle } from '@/services/analyze-bottle'; @@ -17,7 +18,7 @@ import Link from 'next/link'; import { useI18n } from '@/i18n/I18nContext'; import { useSession } from '@/context/SessionContext'; import { shortenCategory } from '@/lib/format'; - +import { magicScan } from '@/services/magic-scan'; interface CameraCaptureProps { onImageCaptured?: (base64Image: string) => void; onAnalysisComplete?: (data: BottleMetadata) => void; @@ -63,6 +64,23 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS const [wbDiscovery, setWbDiscovery] = useState<{ id: string; url: string; title: string } | null>(null); const [isDiscovering, setIsDiscovering] = useState(false); const [originalFile, setOriginalFile] = useState(null); + const [isAdmin, setIsAdmin] = useState(false); + const [aiProvider, setAiProvider] = useState<'gemini' | 'nebius'>('gemini'); + + React.useEffect(() => { + const checkAdmin = async () => { + const { data: { user } } = await supabase.auth.getUser(); + if (user) { + const { data } = await supabase + .from('admin_users') + .select('role') + .eq('user_id', user.id) + .maybeSingle(); + setIsAdmin(!!data); + } + }; + checkAdmin(); + }, [supabase]); const handleCapture = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; @@ -116,11 +134,19 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS return; } - const response = await analyzeBottle(compressedBase64); + const response = await magicScan(compressedBase64, aiProvider); if (response.success && response.data) { setAnalysisResult(response.data); + if (response.wb_id) { + setWbDiscovery({ + id: response.wb_id, + url: `https://www.whiskybase.com/whiskies/whisky/${response.wb_id}`, + title: `${response.data.distillery || ''} ${response.data.name || ''}` + }); + } + // Duplicate Check const match = await findMatchingBottle(response.data); if (match) { @@ -299,7 +325,26 @@ export default function CameraCapture({ onImageCaptured, onAnalysisComplete, onS return (
-

{t('camera.magicShot')}

+
+

{t('camera.magicShot')}

+ + {isAdmin && ( +
+ + +
+ )} +
{ - const activeBottles = bottles.filter(b => b.status !== 'empty'); const totalValue = bottles.reduce((sum, b) => sum + (Number(b.purchase_price) || 0), 0); const ratings = bottles.flatMap(b => b.tastings?.map(t => t.rating) || []); @@ -38,7 +36,6 @@ export default function StatsDashboard({ bottles }: StatsDashboardProps) { return { totalValue, - activeCount: activeBottles.length, avgRating, topDistillery, totalCount: bottles.length @@ -55,7 +52,7 @@ export default function StatsDashboard({ bottles }: StatsDashboardProps) { }, { label: t('home.stats.activeBottles'), - value: stats.activeCount, + value: stats.totalCount, icon: Home, color: 'text-blue-600', bg: 'bg-blue-50 dark:bg-blue-900/20' diff --git a/src/components/StatusSwitcher.tsx b/src/components/StatusSwitcher.tsx deleted file mode 100644 index be6cd15..0000000 --- a/src/components/StatusSwitcher.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client'; - -import React, { useState } from 'react'; -import { updateBottleStatus } from '@/services/update-bottle-status'; -import { Loader2, Package, Play, CheckCircle, FlaskConical } from 'lucide-react'; -import { useI18n } from '@/i18n/I18nContext'; - -interface StatusSwitcherProps { - bottleId: string; - currentStatus: 'sealed' | 'open' | 'sampled' | 'empty'; -} - -export default function StatusSwitcher({ bottleId, currentStatus }: StatusSwitcherProps) { - const { t } = useI18n(); - const [status, setStatus] = useState(currentStatus); - const [loading, setLoading] = useState(false); - - const handleStatusChange = async (newStatus: 'sealed' | 'open' | 'sampled' | 'empty') => { - if (newStatus === status || loading) return; - - setLoading(true); - try { - const result = await updateBottleStatus(bottleId, newStatus); - if (result.success) { - setStatus(newStatus); - } else { - alert(result.error || t('common.error')); - } - } catch (err) { - alert(t('common.error')); - } finally { - setLoading(false); - } - }; - - const options = [ - { id: 'sealed', label: t('bottle.status.sealed'), icon: Package, color: 'hover:bg-blue-500' }, - { id: 'open', label: t('bottle.status.open'), icon: Play, color: 'hover:bg-amber-500' }, - { id: 'sampled', label: t('bottle.status.sampled'), icon: FlaskConical, color: 'hover:bg-purple-500' }, - { id: 'empty', label: t('bottle.status.empty'), icon: CheckCircle, color: 'hover:bg-zinc-500' }, - ] as const; - - return ( -
-
- - {loading && } -
-
- {options.map((opt) => { - const Icon = opt.icon; - const isActive = status === opt.id; - return ( - - ); - })} -
-
- ); -} diff --git a/src/components/TagSelector.tsx b/src/components/TagSelector.tsx new file mode 100644 index 0000000..a8d4877 --- /dev/null +++ b/src/components/TagSelector.tsx @@ -0,0 +1,151 @@ +'use client'; + +import React, { useState, useEffect, useMemo } from 'react'; +import { Tag, TagCategory, getTagsByCategory, createCustomTag } from '@/services/tags'; +import { X, Plus, Search, Check, Loader2, Sparkles } from 'lucide-react'; +import { useI18n } from '@/i18n/I18nContext'; + +interface TagSelectorProps { + category: TagCategory; + selectedTagIds: string[]; + onToggleTag: (tagId: string) => void; + label?: string; +} + +export default function TagSelector({ category, selectedTagIds, onToggleTag, label }: TagSelectorProps) { + const { t } = useI18n(); + const [tags, setTags] = useState([]); + const [search, setSearch] = useState(''); + const [isLoading, setIsLoading] = useState(true); + const [isCreating, setIsCreating] = useState(false); + + useEffect(() => { + const fetchTags = async () => { + setIsLoading(true); + const data = await getTagsByCategory(category); + setTags(data); + setIsLoading(false); + }; + fetchTags(); + }, [category]); + + const filteredTags = useMemo(() => { + if (!search) return tags; + const s = search.toLowerCase(); + return tags.filter(tag => { + const rawMatch = tag.name.toLowerCase().includes(s); + const translatedMatch = tag.is_system_default && t(`aroma.${tag.name}`).toLowerCase().includes(s); + return rawMatch || translatedMatch; + }); + }, [tags, search, t]); + + const handleCreateTag = async () => { + if (!search || isCreating) return; + + setIsCreating(true); + const result = await createCustomTag(search, category); + if (result.success && result.tag) { + setTags(prev => [...prev, result.tag!]); + onToggleTag(result.tag!.id); + setSearch(''); + } + setIsCreating(false); + }; + + const selectedTags = tags.filter(t => selectedTagIds.includes(t.id)); + + return ( +
+ {label && ( + + )} + + {/* Selected Tags */} +
+ {selectedTags.length > 0 ? ( + selectedTags.map(tag => ( + + )) + ) : ( + Noch keine Tags gewählt... + )} +
+ + {/* Search and Suggest */} +
+
+ + setSearch(e.target.value)} + placeholder="Tag suchen oder hinzufügen..." + className="w-full pl-9 pr-4 py-2 bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-xl text-xs focus:ring-2 focus:ring-amber-500 outline-none transition-all dark:text-zinc-200 placeholder:text-zinc-400" + /> + {isCreating && ( + + )} +
+ + {search && ( +
+
+ {filteredTags.length > 0 ? ( + filteredTags.map(tag => ( + + )) + ) : ( + + )} +
+
+ )} +
+ + {/* Suggestions Chips (limit to 6 random or most common) */} + {!search && tags.length > 0 && ( +
+ {tags + .filter(t => !selectedTagIds.includes(t.id)) + .slice(0, 8) + .map(tag => ( + + ))} +
+ )} +
+ ); +} diff --git a/src/components/TastingList.tsx b/src/components/TastingList.tsx index 53af076..918d79f 100644 --- a/src/components/TastingList.tsx +++ b/src/components/TastingList.tsx @@ -16,7 +16,7 @@ interface Tasting { is_sample?: boolean; bottle_id: string; created_at: string; - tasting_tags?: { + tasting_buddies?: { buddies: { id: string; name: string; @@ -26,6 +26,14 @@ interface Tasting { id: string; name: string; }; + tasting_tags?: { + tags: { + id: string; + name: string; + category: string; + is_system_default: boolean; + } + }[]; user_id: string; } @@ -188,13 +196,30 @@ export default function TastingList({ initialTastings, currentUserId }: TastingL
{note.tasting_tags && note.tasting_tags.length > 0 && ( +
+ {note.tasting_tags.map(tt => ( + + {tt.tags.is_system_default ? t(`aroma.${tt.tags.name}`) : tt.tags.name} + + ))} +
+ )} + + {note.tasting_buddies && note.tasting_buddies.length > 0 && (
{t('tasting.with') || 'Mit'}: - tag.buddies.name)} /> + tag.buddies.name)} />
)} diff --git a/src/components/TastingNoteForm.tsx b/src/components/TastingNoteForm.tsx index 50c2692..52f5d40 100644 --- a/src/components/TastingNoteForm.tsx +++ b/src/components/TastingNoteForm.tsx @@ -2,10 +2,11 @@ import React, { useState, useEffect } from 'react'; import { saveTasting } from '@/services/save-tasting'; -import { Loader2, Send, Star, Users, Check, Sparkles, Droplets } from 'lucide-react'; +import { Loader2, Send, Star, Users, Check, Sparkles, Droplets, Wind, Utensils, Zap } from 'lucide-react'; import { createClientComponentClient } from '@supabase/auth-helpers-nextjs'; import { useI18n } from '@/i18n/I18nContext'; import { useSession } from '@/context/SessionContext'; +import TagSelector from './TagSelector'; interface Buddy { id: string; @@ -29,6 +30,9 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm const [error, setError] = useState(null); const [buddies, setBuddies] = useState([]); const [selectedBuddyIds, setSelectedBuddyIds] = useState([]); + const [noseTagIds, setNoseTagIds] = useState([]); + const [palateTagIds, setPalateTagIds] = useState([]); + const [finishTagIds, setFinishTagIds] = useState([]); const { activeSession } = useSession(); const effectiveSessionId = sessionId || activeSession?.id; @@ -62,6 +66,18 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm ); }; + const toggleNoseTag = (id: string) => { + setNoseTagIds(prev => prev.includes(id) ? prev.filter(tid => tid !== id) : [...prev, id]); + }; + + const togglePalateTag = (id: string) => { + setPalateTagIds(prev => prev.includes(id) ? prev.filter(tid => tid !== id) : [...prev, id]); + }; + + const toggleFinishTag = (id: string) => { + setFinishTagIds(prev => prev.includes(id) ? prev.filter(tid => tid !== id) : [...prev, id]); + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLoading(true); @@ -77,6 +93,7 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm finish_notes: finish, is_sample: isSample, buddy_ids: selectedBuddyIds, + tag_ids: [...noseTagIds, ...palateTagIds, ...finishTagIds], }); if (result.success) { @@ -84,6 +101,9 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm setPalate(''); setFinish(''); setSelectedBuddyIds([]); + setNoseTagIds([]); + setPalateTagIds([]); + setFinishTagIds([]); // We don't need to manually refresh because of revalidatePath in the server action } else { setError(result.error || t('common.error')); @@ -158,37 +178,71 @@ export default function TastingNoteForm({ bottleId, sessionId }: TastingNoteForm
-
- -