diff --git a/database.types.ts b/database.types.ts index fe554400..75e21336 100644 --- a/database.types.ts +++ b/database.types.ts @@ -1,10 +1,10 @@ export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]; export type Database = { - // Allows to automatically instanciate createClient with right options + // Allows to automatically instantiate createClient with right options // instead of createClient(URL, KEY) __InternalSupabase: { - PostgrestVersion: '12.2.3 (519615d)'; + PostgrestVersion: '13.0.4'; }; public: { Tables: { @@ -43,264 +43,348 @@ export type Database = { } ]; }; - communication_methods: { + babylon_connection_types: { Row: { - communication_method_id: number; created_at: string; id: number; - is_active: boolean; name: string; + type_id: number; }; Insert: { - communication_method_id?: number; created_at?: string; id?: number; - is_active?: boolean; name: string; + type_id?: number; }; Update: { - communication_method_id?: number; created_at?: string; id?: number; - is_active?: boolean; name?: string; + type_id?: number; }; Relationships: []; }; - customers: { + babylon_decoders: { Row: { - created_at: string | null; - email: string | null; - id: string; - name: string | null; - stripe_customer_id: string; - updated_at: string | null; - user_id: string; + created_at: string; + decoder: string | null; + decoder_id: number; + id: number; + name: string; }; Insert: { - created_at?: string | null; - email?: string | null; - id: string; - name?: string | null; - stripe_customer_id: string; - updated_at?: string | null; - user_id: string; + created_at?: string; + decoder?: string | null; + decoder_id?: number; + id?: number; + name?: string; }; Update: { - created_at?: string | null; - email?: string | null; - id?: string; - name?: string | null; - stripe_customer_id?: string; - updated_at?: string | null; - user_id?: string; + created_at?: string; + decoder?: string | null; + decoder_id?: number; + id?: number; + name?: string; }; - Relationships: [ - { - foreignKeyName: 'customers_user_id_fkey'; - columns: ['user_id']; - isOneToOne: false; - referencedRelation: 'profiles'; - referencedColumns: ['id']; - } - ]; + Relationships: []; }; - cw_air_data: { + babylon_in_connections: { Row: { - co: number | null; - co2: number | null; + connection_id: number; + connection_name: string; created_at: string; - dev_eui: string; - humidity: number | null; - is_simulated: boolean; - lux: number | null; - pressure: number | null; - rainfall: number | null; - temperature_c: number | null; - uv_index: number | null; - wind_direction: number | null; - wind_speed: number | null; + endpoint: string; + id: number; + password: string | null; + port: number | null; + profile_id: string; + type: number; + username: string | null; }; Insert: { - co?: number | null; - co2?: number | null; + connection_id?: number; + connection_name: string; created_at?: string; - dev_eui: string; - humidity?: number | null; - is_simulated?: boolean; - lux?: number | null; - pressure?: number | null; - rainfall?: number | null; - temperature_c?: number | null; - uv_index?: number | null; - wind_direction?: number | null; - wind_speed?: number | null; + endpoint: string; + id?: number; + password?: string | null; + port?: number | null; + profile_id: string; + type: number; + username?: string | null; }; Update: { - co?: number | null; - co2?: number | null; + connection_id?: number; + connection_name?: string; created_at?: string; - dev_eui?: string; - humidity?: number | null; - is_simulated?: boolean; - lux?: number | null; - pressure?: number | null; - rainfall?: number | null; - temperature_c?: number | null; - uv_index?: number | null; - wind_direction?: number | null; - wind_speed?: number | null; + endpoint?: string; + id?: number; + password?: string | null; + port?: number | null; + profile_id?: string; + type?: number; + username?: string | null; }; - Relationships: []; + Relationships: [ + { + foreignKeyName: 'public_babylon_connections_profile_id_fkey'; + columns: ['profile_id']; + isOneToOne: false; + referencedRelation: 'profiles'; + referencedColumns: ['id']; + }, + { + foreignKeyName: 'public_babylon_in_connections_type_fkey'; + columns: ['type']; + isOneToOne: false; + referencedRelation: 'babylon_connection_types'; + referencedColumns: ['type_id']; + } + ]; }; - cw_air_thvd: { + babylon_input_output: { Row: { created_at: string; - dev_eui: string; - dewPointC: number | null; - humidity: number; id: number; - profile_id: string | null; - temperatureC: number; - vpd: number | null; + in_id: number; + out_id: number; }; Insert: { created_at?: string; - dev_eui: string; - dewPointC?: number | null; - humidity: number; id?: number; - profile_id?: string | null; - temperatureC: number; - vpd?: number | null; + in_id: number; + out_id: number; }; Update: { created_at?: string; - dev_eui?: string; - dewPointC?: number | null; - humidity?: number; id?: number; - profile_id?: string | null; - temperatureC?: number; - vpd?: number | null; + in_id?: number; + out_id?: number; }; Relationships: [ { - foreignKeyName: 'cw_air_thvd_dev_eui_fkey'; - columns: ['dev_eui']; + foreignKeyName: 'babylon_input_output_in_id_fkey'; + columns: ['in_id']; isOneToOne: false; - referencedRelation: 'cw_devices'; - referencedColumns: ['dev_eui']; + referencedRelation: 'babylon_in_connections'; + referencedColumns: ['connection_id']; }, { - foreignKeyName: 'public_cw_air_thvd_profile_id_fkey'; - columns: ['profile_id']; + foreignKeyName: 'babylon_input_output_out_id_fkey'; + columns: ['out_id']; isOneToOne: false; - referencedRelation: 'profiles'; - referencedColumns: ['id']; + referencedRelation: 'babylon_out_connections'; + referencedColumns: ['connection_id']; } ]; }; - cw_co2_alerts: { + babylon_notifiers: { Row: { - action: string; - cleared: boolean; + api_key: string | null; created_at: string; - dev_eui: string; + host: string | null; id: number; - OneSignalID: string | null; - operator: string; + isSecure: boolean; + name: string; + notifier_id: number | null; + password: string | null; + port: number | null; + type: number | null; + username: string | null; + }; + Insert: { + api_key?: string | null; + created_at?: string; + host?: string | null; + id?: number; + isSecure?: boolean; + name: string; + notifier_id?: number | null; + password?: string | null; + port?: number | null; + type?: number | null; + username?: string | null; + }; + Update: { + api_key?: string | null; + created_at?: string; + host?: string | null; + id?: number; + isSecure?: boolean; + name?: string; + notifier_id?: number | null; + password?: string | null; + port?: number | null; + type?: number | null; + username?: string | null; + }; + Relationships: []; + }; + babylon_notifiers_out_connections: { + Row: { + created_at: string; + id: number; + notifier_id: number; + out_connection_id: number; + }; + Insert: { + created_at?: string; + id?: number; + notifier_id: number; + out_connection_id: number; + }; + Update: { + created_at?: string; + id?: number; + notifier_id?: number; + out_connection_id?: number; + }; + Relationships: []; + }; + babylon_out_connections: { + Row: { + connection_id: number; + connection_name: string; + created_at: string; + decoder: number | null; + endpoint: string | null; + id: number; + password: string | null; + port: number | null; profile_id: string; - receiver: string; - subject: string; - value: number; + type: number; + username: string | null; }; Insert: { - action: string; - cleared: boolean; + connection_id?: number; + connection_name: string; created_at?: string; - dev_eui: string; + decoder?: number | null; + endpoint?: string | null; id?: number; - OneSignalID?: string | null; - operator: string; + password?: string | null; + port?: number | null; profile_id: string; - receiver: string; - subject: string; - value: number; + type: number; + username?: string | null; }; Update: { - action?: string; - cleared?: boolean; + connection_id?: number; + connection_name?: string; created_at?: string; - dev_eui?: string; + decoder?: number | null; + endpoint?: string | null; id?: number; - OneSignalID?: string | null; - operator?: string; + password?: string | null; + port?: number | null; profile_id?: string; - receiver?: string; - subject?: string; - value?: number; + type?: number; + username?: string | null; }; Relationships: [ { - foreignKeyName: 'cw_co2_alerts_dev_eui_fkey'; - columns: ['dev_eui']; + foreignKeyName: 'public_babylon_out_connections_decoder_fkey'; + columns: ['decoder']; isOneToOne: false; - referencedRelation: 'cw_devices'; - referencedColumns: ['dev_eui']; + referencedRelation: 'babylon_decoders'; + referencedColumns: ['decoder_id']; }, { - foreignKeyName: 'cw_co2_alerts_profile_id_fkey'; + foreignKeyName: 'public_babylon_out_connections_profile_id_fkey'; columns: ['profile_id']; isOneToOne: false; referencedRelation: 'profiles'; referencedColumns: ['id']; + }, + { + foreignKeyName: 'public_babylon_out_connections_type_fkey'; + columns: ['type']; + isOneToOne: false; + referencedRelation: 'babylon_connection_types'; + referencedColumns: ['type_id']; } ]; }; - cw_co2_uplinks: { + communication_methods: { Row: { - battery: number | null; - co2_level: number | null; + communication_method_id: number; created_at: string; - dev_eui: string | null; - humidity: number; id: number; - pressure: number | null; - profile_id: string | null; - temperature: number; + is_active: boolean; + name: string; }; Insert: { - battery?: number | null; - co2_level?: number | null; + communication_method_id?: number; created_at?: string; - dev_eui?: string | null; - humidity: number; id?: number; - pressure?: number | null; - profile_id?: string | null; - temperature: number; + is_active?: boolean; + name: string; }; Update: { - battery?: number | null; - co2_level?: number | null; + communication_method_id?: number; created_at?: string; - dev_eui?: string | null; - humidity?: number; id?: number; + is_active?: boolean; + name?: string; + }; + Relationships: []; + }; + cw_air_data: { + Row: { + battery_level: number | null; + co: number | null; + co2: number | null; + created_at: string; + dev_eui: string; + humidity: number | null; + is_simulated: boolean; + lux: number | null; + pressure: number | null; + rainfall: number | null; + smoke_detected: boolean | null; + temperature_c: number | null; + uv_index: number | null; + vape_detected: boolean | null; + wind_direction: number | null; + wind_speed: number | null; + }; + Insert: { + battery_level?: number | null; + co?: number | null; + co2?: number | null; + created_at?: string; + dev_eui: string; + humidity?: number | null; + is_simulated?: boolean; + lux?: number | null; pressure?: number | null; - profile_id?: string | null; - temperature?: number; + rainfall?: number | null; + smoke_detected?: boolean | null; + temperature_c?: number | null; + uv_index?: number | null; + vape_detected?: boolean | null; + wind_direction?: number | null; + wind_speed?: number | null; }; - Relationships: [ - { - foreignKeyName: 'cw_co2_uplinks_dev_eui_fkey'; - columns: ['dev_eui']; - isOneToOne: false; - referencedRelation: 'cw_devices'; - referencedColumns: ['dev_eui']; - } - ]; + Update: { + battery_level?: number | null; + co?: number | null; + co2?: number | null; + created_at?: string; + dev_eui?: string; + humidity?: number | null; + is_simulated?: boolean; + lux?: number | null; + pressure?: number | null; + rainfall?: number | null; + smoke_detected?: boolean | null; + temperature_c?: number | null; + uv_index?: number | null; + vape_detected?: boolean | null; + wind_direction?: number | null; + wind_speed?: number | null; + }; + Relationships: []; }; cw_data_metadata: { Row: { @@ -503,7 +587,6 @@ export type Database = { name: string; report_endpoint: string | null; serial_number: string | null; - tti_name: string | null; type: number | null; upload_interval: number | null; user_id: string | null; @@ -520,7 +603,6 @@ export type Database = { name?: string; report_endpoint?: string | null; serial_number?: string | null; - tti_name?: string | null; type?: number | null; upload_interval?: number | null; user_id?: string | null; @@ -537,7 +619,6 @@ export type Database = { name?: string; report_endpoint?: string | null; serial_number?: string | null; - tti_name?: string | null; type?: number | null; upload_interval?: number | null; user_id?: string | null; @@ -628,6 +709,7 @@ export type Database = { }; cw_location_owners: { Row: { + admin_user_id: string; description: string | null; id: number; is_active: boolean | null; @@ -637,6 +719,7 @@ export type Database = { user_id: string; }; Insert: { + admin_user_id: string; description?: string | null; id?: number; is_active?: boolean | null; @@ -646,6 +729,7 @@ export type Database = { user_id: string; }; Update: { + admin_user_id?: string; description?: string | null; id?: number; is_active?: boolean | null; @@ -718,18 +802,21 @@ export type Database = { id: number; name: string; notifier_id: number; + notifier_value: number; }; Insert: { created_at?: string; id?: number; name: string; - notifier_id?: number; + notifier_id: number; + notifier_value: number; }; Update: { created_at?: string; id?: number; name?: string; notifier_id?: number; + notifier_value?: number; }; Relationships: []; }; @@ -754,36 +841,71 @@ export type Database = { }; Relationships: []; }; + cw_pulse_meters: { + Row: { + count: number; + created_at: string; + dev_eui: string; + id: number; + litersPerPulse: number; + periodCount: number; + }; + Insert: { + count?: number; + created_at?: string; + dev_eui: string; + id?: number; + litersPerPulse?: number; + periodCount?: number; + }; + Update: { + count?: number; + created_at?: string; + dev_eui?: string; + id?: number; + litersPerPulse?: number; + periodCount?: number; + }; + Relationships: [ + { + foreignKeyName: 'public_cw_pulse_meters_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: false; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + } + ]; + }; cw_relay_data: { Row: { created_at: string; dev_eui: string; id: number; - relay_1: boolean; + last_update: string; + relay_1: boolean | null; relay_2: boolean | null; - relay_3: boolean | null; }; Insert: { created_at?: string; dev_eui: string; id?: number; - relay_1?: boolean; + last_update: string; + relay_1?: boolean | null; relay_2?: boolean | null; - relay_3?: boolean | null; }; Update: { created_at?: string; dev_eui?: string; id?: number; - relay_1?: boolean; + last_update?: string; + relay_1?: boolean | null; relay_2?: boolean | null; - relay_3?: boolean | null; }; Relationships: [ { foreignKeyName: 'cw_relay_data_dev_eui_fkey'; columns: ['dev_eui']; - isOneToOne: false; + isOneToOne: true; referencedRelation: 'cw_devices'; referencedColumns: ['dev_eui']; } @@ -845,6 +967,7 @@ export type Database = { notifier_type: number; profile_id: string; ruleGroupId: string; + send_using: string | null; trigger_count: number; }; Insert: { @@ -858,6 +981,7 @@ export type Database = { notifier_type: number; profile_id?: string; ruleGroupId: string; + send_using?: string | null; trigger_count?: number; }; Update: { @@ -871,6 +995,7 @@ export type Database = { notifier_type?: number; profile_id?: string; ruleGroupId?: string; + send_using?: string | null; trigger_count?: number; }; Relationships: [ @@ -924,95 +1049,6 @@ export type Database = { }; Relationships: []; }; - cw_soil_uplinks: { - Row: { - battery: number | null; - created_at: string; - dev_eui: string | null; - ec: number | null; - id: number; - internal_temp: number | null; - k: number | null; - moisture: number; - n: number | null; - p: number | null; - ph: number | null; - read_attempts: number | null; - real_duration: number | null; - temperature: number; - }; - Insert: { - battery?: number | null; - created_at?: string; - dev_eui?: string | null; - ec?: number | null; - id?: number; - internal_temp?: number | null; - k?: number | null; - moisture: number; - n?: number | null; - p?: number | null; - ph?: number | null; - read_attempts?: number | null; - real_duration?: number | null; - temperature: number; - }; - Update: { - battery?: number | null; - created_at?: string; - dev_eui?: string | null; - ec?: number | null; - id?: number; - internal_temp?: number | null; - k?: number | null; - moisture?: number; - n?: number | null; - p?: number | null; - ph?: number | null; - read_attempts?: number | null; - real_duration?: number | null; - temperature?: number; - }; - Relationships: [ - { - foreignKeyName: 'cw_soil_uplinks_dev_eui_fkey'; - columns: ['dev_eui']; - isOneToOne: false; - referencedRelation: 'cw_devices'; - referencedColumns: ['dev_eui']; - } - ]; - }; - cw_traffic: { - Row: { - created_at: string; - dev_eui: string; - id: number; - object_type: string; - period_in: number; - period_out: number; - period_total: number; - }; - Insert: { - created_at?: string; - dev_eui: string; - id?: number; - object_type: string; - period_in?: number; - period_out?: number; - period_total?: number; - }; - Update: { - created_at?: string; - dev_eui?: string; - id?: number; - object_type?: string; - period_in?: number; - period_out?: number; - period_total?: number; - }; - Relationships: []; - }; cw_traffic2: { Row: { bicycle_count: number; @@ -1130,33 +1166,42 @@ export type Database = { } ]; }; - device_data_sample: { + devices: { Row: { - co2_level: number | null; - created_at: string; + active: boolean; + created_at: string | null; dev_eui: string; - humidity: number | null; + device_name: string | null; id: number; - pressure: number | null; - temperature_c: number | null; + lat: number | null; + linked_device_eui: string | null; + lng: number | null; + profile_id: string | null; + type: string | null; }; Insert: { - co2_level?: number | null; - created_at: string; + active?: boolean; + created_at?: string | null; dev_eui: string; - humidity?: number | null; + device_name?: string | null; id?: number; - pressure?: number | null; - temperature_c?: number | null; + lat?: number | null; + linked_device_eui?: string | null; + lng?: number | null; + profile_id?: string | null; + type?: string | null; }; Update: { - co2_level?: number | null; - created_at?: string; + active?: boolean; + created_at?: string | null; dev_eui?: string; - humidity?: number | null; + device_name?: string | null; id?: number; - pressure?: number | null; - temperature_c?: number | null; + lat?: number | null; + linked_device_eui?: string | null; + lng?: number | null; + profile_id?: string | null; + type?: string | null; }; Relationships: []; }; @@ -1231,110 +1276,6 @@ export type Database = { }; Relationships: []; }; - netvox_ra02a: { - Row: { - battery: number; - created_at: string; - dev_eui: string; - fireAlarm: number; - gateway_count: number | null; - highTempAlarm: number; - id: number; - profile_id: string | null; - rssi: number | null; - snr: number | null; - temperatureC: number; - }; - Insert: { - battery: number; - created_at?: string; - dev_eui: string; - fireAlarm: number; - gateway_count?: number | null; - highTempAlarm: number; - id?: number; - profile_id?: string | null; - rssi?: number | null; - snr?: number | null; - temperatureC: number; - }; - Update: { - battery?: number; - created_at?: string; - dev_eui?: string; - fireAlarm?: number; - gateway_count?: number | null; - highTempAlarm?: number; - id?: number; - profile_id?: string | null; - rssi?: number | null; - snr?: number | null; - temperatureC?: number; - }; - Relationships: [ - { - foreignKeyName: 'netvox_ra02a_profile_id_fkey'; - columns: ['profile_id']; - isOneToOne: false; - referencedRelation: 'profiles'; - referencedColumns: ['id']; - } - ]; - }; - payments: { - Row: { - amount: number; - created_at: string | null; - currency: string; - id: string; - status: string; - stripe_invoice_id: string | null; - stripe_payment_intent_id: string; - subscription_id: string | null; - updated_at: string | null; - user_id: string; - }; - Insert: { - amount: number; - created_at?: string | null; - currency?: string; - id: string; - status: string; - stripe_invoice_id?: string | null; - stripe_payment_intent_id: string; - subscription_id?: string | null; - updated_at?: string | null; - user_id: string; - }; - Update: { - amount?: number; - created_at?: string | null; - currency?: string; - id?: string; - status?: string; - stripe_invoice_id?: string | null; - stripe_payment_intent_id?: string; - subscription_id?: string | null; - updated_at?: string | null; - user_id?: string; - }; - Relationships: [ - { - foreignKeyName: 'payments_subscription_id_fkey'; - columns: ['subscription_id']; - isOneToOne: false; - referencedRelation: 'subscriptions'; - referencedColumns: ['id']; - }, - { - foreignKeyName: 'payments_user_id_fkey'; - columns: ['user_id']; - isOneToOne: false; - referencedRelation: 'profiles'; - referencedColumns: ['id']; - } - ]; - }; permissions: { Row: { allowed_by_profile_id: string | null; @@ -1390,6 +1331,7 @@ export type Database = { full_name: string | null; id: string; last_login: string | null; + line_id: string | null; updated_at: string | null; username: string | null; website: string | null; @@ -1403,6 +1345,7 @@ export type Database = { full_name?: string | null; id: string; last_login?: string | null; + line_id?: string | null; updated_at?: string | null; username?: string | null; website?: string | null; @@ -1416,6 +1359,7 @@ export type Database = { full_name?: string | null; id?: string; last_login?: string | null; + line_id?: string | null; updated_at?: string | null; username?: string | null; website?: string | null; @@ -1425,7 +1369,7 @@ export type Database = { report_alert_points: { Row: { created_at: string; - data_point_key: string | null; + data_point_key: string; hex_color: string | null; id: number; max: number | null; @@ -1433,12 +1377,12 @@ export type Database = { name: string; operator: string | null; report_id: string; - user_id: string | null; + user_id: string; value: number | null; }; Insert: { created_at?: string; - data_point_key?: string | null; + data_point_key: string; hex_color?: string | null; id?: number; max?: number | null; @@ -1446,12 +1390,12 @@ export type Database = { name: string; operator?: string | null; report_id: string; - user_id?: string | null; + user_id?: string; value?: number | null; }; Update: { created_at?: string; - data_point_key?: string | null; + data_point_key?: string; hex_color?: string | null; id?: number; max?: number | null; @@ -1459,7 +1403,7 @@ export type Database = { name?: string; operator?: string | null; report_id?: string; - user_id?: string | null; + user_id?: string; value?: number | null; }; Relationships: [ @@ -1525,7 +1469,7 @@ export type Database = { end_of_week: boolean; id: number; is_active: boolean; - report_id: string; + report_id: string | null; report_user_schedule_id: number; user_id: string; }; @@ -1536,7 +1480,7 @@ export type Database = { end_of_week?: boolean; id?: number; is_active?: boolean; - report_id: string; + report_id?: string | null; report_user_schedule_id?: number; user_id?: string; }; @@ -1547,7 +1491,7 @@ export type Database = { end_of_week?: boolean; id?: number; is_active?: boolean; - report_id?: string; + report_id?: string | null; report_user_schedule_id?: number; user_id?: string; }; @@ -1749,97 +1693,6 @@ export type Database = { }; Relationships: []; }; - subscription_items: { - Row: { - created_at: string | null; - id: string; - quantity: number | null; - stripe_price_id: string; - stripe_subscription_item_id: string; - subscription_id: string; - updated_at: string | null; - }; - Insert: { - created_at?: string | null; - id: string; - quantity?: number | null; - stripe_price_id: string; - stripe_subscription_item_id: string; - subscription_id: string; - updated_at?: string | null; - }; - Update: { - created_at?: string | null; - id?: string; - quantity?: number | null; - stripe_price_id?: string; - stripe_subscription_item_id?: string; - subscription_id?: string; - updated_at?: string | null; - }; - Relationships: [ - { - foreignKeyName: 'subscription_items_subscription_id_fkey'; - columns: ['subscription_id']; - isOneToOne: false; - referencedRelation: 'subscriptions'; - referencedColumns: ['id']; - } - ]; - }; - subscriptions: { - Row: { - cancel_at_period_end: boolean | null; - canceled_at: string | null; - created_at: string | null; - current_period_end: string | null; - current_period_start: string | null; - customer_id: string; - id: string; - status: string; - stripe_price_id: string; - stripe_subscription_id: string; - updated_at: string | null; - user_id: string; - }; - Insert: { - cancel_at_period_end?: boolean | null; - canceled_at?: string | null; - created_at?: string | null; - current_period_end?: string | null; - current_period_start?: string | null; - customer_id: string; - id: string; - status: string; - stripe_price_id: string; - stripe_subscription_id: string; - updated_at?: string | null; - user_id: string; - }; - Update: { - cancel_at_period_end?: boolean | null; - canceled_at?: string | null; - created_at?: string | null; - current_period_end?: string | null; - current_period_start?: string | null; - customer_id?: string; - id?: string; - status?: string; - stripe_price_id?: string; - stripe_subscription_id?: string; - updated_at?: string | null; - user_id?: string; - }; - Relationships: [ - { - foreignKeyName: 'subscriptions_user_id_fkey'; - columns: ['user_id']; - isOneToOne: false; - referencedRelation: 'profiles'; - referencedColumns: ['id']; - } - ]; - }; user_discord_connections: { Row: { access_token: string; @@ -1889,100 +1742,59 @@ export type Database = { Args: { bucket: string; object: string }; Returns: Record; }; - get_filtered_device_report_data: { - Args: { - dev_id: string; - start_time: string; - end_time: string; - interval_minutes: number; - target_column: string; - compare_operator: string; - compare_value_min: number; - compare_value_max?: number; - }; - Returns: Json[]; - }; get_filtered_device_report_data_multi: { Args: { + p_columns: string[]; p_dev_id: string; - p_start_time: string; p_end_time: string; p_interval_minutes: number; - p_columns: string[]; - p_ops: string[]; - p_mins: number[]; p_maxs: number[]; + p_mins: number[]; + p_ops: string[]; + p_start_time: string; }; Returns: Json[]; }; get_hloc_data: { Args: | { - p_dev_eui: string; - p_bucket_interval: string; - p_time_range: string; - p_metric: string; + device_eui: string; + end_time: string; + start_time: string; + table_name: string; + time_interval: string; } | { - p_dev_eui: string; p_bucket_interval: string; - p_time_range: string; + p_dev_eui: string; p_metric: string; p_table: string; + p_time_range: string; } | { - start_time: string; - end_time: string; - time_interval: string; - table_name: string; - device_eui: string; + p_bucket_interval: string; + p_dev_eui: string; + p_metric: string; + p_time_range: string; }; Returns: { bucket: string; - dev_eui: string; - open_val: number; close_val: number; - low_val: number; + dev_eui: string; high_val: number; + low_val: number; + open_val: number; }[]; }; get_location_for_user: { Args: { user_id: string }; Returns: number[]; }; - get_multi_condition_device_report_data: { - Args: { - p_dev_id: string; - p_report_id: string; - p_start_time: string; - p_end_time: string; - p_interval_minutes: number; - }; - Returns: Json[]; - }; - get_report_data_for_device: { - Args: - | { input_dev_eui: string; input_start: string; input_end: string } - | { - input_dev_eui: string; - input_start: string; - input_end: string; - input_timezone?: string; - } - | { - input_dev_eui: string; - input_start: string; - input_end: string; - input_timezone?: string; - input_interval_minutes?: number; - }; - Returns: Json; - }; get_road_events: { Args: { time_grouping: string }; Returns: { - group_period: string; event_count: number; + group_period: string; }[]; }; get_road_events_summary1: { @@ -1994,8 +1806,8 @@ export type Database = { time_span: string; }; Returns: { - period_start: string; count: number; + period_start: string; }[]; }; }; diff --git a/package.json b/package.json index 387b7c60..e4279bf6 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "globals": "^16.0.0", "husky": "^8.0.0", "jsdom": "^26.0.0", + "supabase": "^2.33.9", "lint-staged": "^16.1.2", "prettier": "^3.4.2", "prettier-plugin-svelte": "^3.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e20c760..aafd141f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -177,6 +177,9 @@ importers: prettier-plugin-tailwindcss: specifier: ^0.6.11 version: 0.6.13(prettier-plugin-svelte@3.4.0(prettier@3.5.3)(svelte@5.34.7))(prettier@3.5.3) + supabase: + specifier: ^2.33.9 + version: 2.33.9 svelte: specifier: ^5.0.0 version: 5.34.7 @@ -2237,6 +2240,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bin-links@5.0.0: + resolution: {integrity: sha512-sdleLVfCjBtgO5cNjA2HVRvWBJAHs4zwenaCPMNJAJU0yNxpzj80IpjOIimkpkr+mhlA+how5poQtt53PygbHA==} + engines: {node: ^18.17.0 || >=20.5.0} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2396,6 +2403,10 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + cmd-shim@7.0.0: + resolution: {integrity: sha512-rtpaCbr164TPPh+zFdkWpCyZuKkjpAzODfaZCf/SVJZzJN+4bHQb/LP3Jzq5/+84um3XXY8r548XiWKSborwVw==} + engines: {node: ^18.17.0 || >=20.5.0} + code-red@1.0.4: resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==} @@ -2649,6 +2660,10 @@ packages: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -2985,6 +3000,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fflate@0.8.2: resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==} @@ -3041,6 +3060,10 @@ packages: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} @@ -3851,6 +3874,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-gyp-build@4.8.4: resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} hasBin: true @@ -3867,6 +3894,10 @@ packages: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} + npm-normalize-package-bin@4.0.0: + resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==} + engines: {node: ^18.17.0 || >=20.5.0} + nwsapi@2.2.20: resolution: {integrity: sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==} @@ -4187,6 +4218,10 @@ packages: resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} engines: {node: '>=6'} + proc-log@5.0.0: + resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} + engines: {node: ^18.17.0 || >=20.5.0} + process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} @@ -4302,6 +4337,10 @@ packages: read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + read-cmd-shim@5.0.0: + resolution: {integrity: sha512-SEbJV7tohp3DAAILbEMPXavBjAnMN0tVnh4+9G8ihV4Pq3HYF9h8QNez9zkJ1ILkv9G2BjdzwctznGZXgu/HGw==} + engines: {node: ^18.17.0 || >=20.5.0} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -4584,6 +4623,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions sourcemap-codec@1.4.8: resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} @@ -4697,6 +4737,11 @@ packages: engines: {node: '>=16 || 14 >=14.17'} hasBin: true + supabase@2.33.9: + resolution: {integrity: sha512-bjCdzcAzbzmPn5B4FNjsAE32aHDgCHtHngj0eDZdZ1+tVbH1/4TwGeZWy41JeiraNx5VPMG+BUOG2VNBXXcXEA==} + engines: {npm: '>=8'} + hasBin: true + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -5319,6 +5364,10 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + write-file-atomic@6.0.0: + resolution: {integrity: sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==} + engines: {node: ^18.17.0 || >=20.5.0} + ws@8.18.2: resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} engines: {node: '>=10.0.0'} @@ -7825,6 +7874,14 @@ snapshots: base64-js@1.5.1: {} + bin-links@5.0.0: + dependencies: + cmd-shim: 7.0.0 + npm-normalize-package-bin: 4.0.0 + proc-log: 5.0.0 + read-cmd-shim: 5.0.0 + write-file-atomic: 6.0.0 + binary-extensions@2.3.0: {} bindings@1.5.0: @@ -8005,6 +8062,8 @@ snapshots: clsx@2.1.1: {} + cmd-shim@7.0.0: {} + code-red@1.0.4: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -8264,6 +8323,8 @@ snapshots: es5-ext: 0.10.64 type: 2.7.3 + data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -8704,6 +8765,11 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fflate@0.8.2: {} file-entry-cache@8.0.0: @@ -8765,6 +8831,10 @@ snapshots: format@0.2.2: {} + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + fs-constants@1.0.0: {} fs-extra@9.1.0: @@ -9521,6 +9591,12 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-gyp-build@4.8.4: {} node-releases@2.0.19: {} @@ -9531,6 +9607,8 @@ snapshots: normalize-path@3.0.0: {} + npm-normalize-package-bin@4.0.0: {} + nwsapi@2.2.20: {} object-assign@4.1.1: {} @@ -9778,6 +9856,8 @@ snapshots: prismjs@1.30.0: {} + proc-log@5.0.0: {} + process@0.11.10: {} prop-types@15.8.1: @@ -9897,6 +9977,8 @@ snapshots: dependencies: pify: 2.3.0 + read-cmd-shim@5.0.0: {} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -10352,6 +10434,15 @@ snapshots: pirates: 4.0.7 ts-interface-checker: 0.1.13 + supabase@2.33.9: + dependencies: + bin-links: 5.0.0 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + tar: 7.4.3 + transitivePeerDependencies: + - supports-color + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -11220,6 +11311,11 @@ snapshots: wrappy@1.0.2: {} + write-file-atomic@6.0.0: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@6.0.5): optionalDependencies: bufferutil: 4.0.9 diff --git a/src/app.css b/src/app.css index 848fc9c4..42dcfd13 100644 --- a/src/app.css +++ b/src/app.css @@ -228,4 +228,23 @@ a { /* override button for event calendar */ .ec-dayGridMonth, .ec-today { display: none !important; +} + +/* in your global CSS (e.g. app.css), *after* the Tailwind layers */ +input[type="date"]::-webkit-calendar-picker-indicator { + /* position relative to the */ + position: absolute; + top: 50%; + right: 0.5rem; /* tweak to exactly your taste */ + transform: translateY(-50%); + /* keep it clickable: */ + pointer-events: all; +} + +/* if you need Firefox support (Gecko), you can also add: */ +input[type="date"]::-moz-calendar-picker-indicator { + position: absolute; + top: 50%; + right: 0.5rem; + transform: translateY(-50%); } \ No newline at end of file diff --git a/src/hooks.server.ts b/src/hooks.server.ts index b1a6940f..94790b8d 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -6,6 +6,7 @@ import { createClient } from '@supabase/supabase-js'; const PUBLIC_ROUTES = [ '/offline.html', '/auth', // All routes under /auth/ + '/legal', '/api/auth', // Only authentication-related API routes '/api/webhook', // Webhook endpoints (authenticated via webhook signatures) '/static', // All static assets diff --git a/src/lib/components/DataCard/DataCard.svelte b/src/lib/components/DataCard/DataCard.svelte index bee2bf7c..eb3986a1 100644 --- a/src/lib/components/DataCard/DataCard.svelte +++ b/src/lib/components/DataCard/DataCard.svelte @@ -63,7 +63,7 @@
+ +
+
+ +
+
+

RELAY CONTROL SYSTEM

+
+
+ + {bothBusy() ? 'PROCESSING' : 'READY'} + +
+
+
+ + +
+
+
SYSTEM OVERRIDE
+
+ + +
+
+
+ + +
+ {#each [{ key: 'relay1', label: 'RELAY 01', channel: 'A' }, { key: 'relay2', label: 'RELAY 02', channel: 'B' }] as relay} +
+
+
CH.{relay.channel}
+
+
+ {#if loadingInitial} +
+ {:else} +
+ {/if} +
+ + {relayState[relay.key] ? 'ACTIVE' : 'STANDBY'} + +
+
+ +
{relay.label}
+ + + +
+
+ LOAD + {relayState[relay.key] ? '100%' : '0%'} +
+
+ V + {relayState[relay.key] ? '24.0' : '0.0'} +
+
+
+ {/each} +
+ + + +
+ + diff --git a/src/lib/components/Reports/NumberLine.svelte b/src/lib/components/Reports/NumberLine.svelte index 058a3535..8e0523d9 100644 --- a/src/lib/components/Reports/NumberLine.svelte +++ b/src/lib/components/Reports/NumberLine.svelte @@ -1,9 +1,18 @@ diff --git a/src/lib/components/SiteWideRefreshButton.svelte b/src/lib/components/SiteWideRefreshButton.svelte new file mode 100644 index 00000000..c62ae83b --- /dev/null +++ b/src/lib/components/SiteWideRefreshButton.svelte @@ -0,0 +1,34 @@ + + +{#if displaySafariRefresh} + +{/if} diff --git a/src/lib/components/StatsCard/StatsCard.svelte b/src/lib/components/StatsCard/StatsCard.svelte index 98439475..6e5a1bb6 100644 --- a/src/lib/components/StatsCard/StatsCard.svelte +++ b/src/lib/components/StatsCard/StatsCard.svelte @@ -5,6 +5,7 @@ import { mdiArrowDownBold, mdiArrowUpBold, mdiMinus } from '@mdi/js'; import { _ } from 'svelte-i18n'; import { Icon } from 'svelte-ux'; + import MaterialIcon from '../UI/icons/MaterialIcon.svelte'; type Props = { key: string; @@ -140,26 +141,26 @@
- {$_('Count')}: + {$_('Count')}: {count !== undefined ? count : $_('N/A')}
- {$_('Median')}: + {$_('Median')}: {median !== undefined ? formatNumber({ key, value: median }) + (notation || '') : 'N/A'}
- {$_('Std Dev')}: + {$_('Std Dev')}: {stdDev !== undefined ? formatNumber({ key, value: stdDev }) + (notation || '') : 'N/A'}
- {$_('Range')}: + {$_('Range')}: {max !== undefined && min !== undefined ? formatNumber({ key, value: max - min }) + (notation || '') @@ -171,8 +172,15 @@ {/if} {#if expandable} -
-
+
+
+ {expanded ? $_('Click to collapse') : $_('Click to expand')}
diff --git a/src/lib/components/UI/dashboard/DataRowItem.svelte b/src/lib/components/UI/dashboard/DataRowItem.svelte index e9ba337d..b23e1b67 100644 --- a/src/lib/components/UI/dashboard/DataRowItem.svelte +++ b/src/lib/components/UI/dashboard/DataRowItem.svelte @@ -128,46 +128,47 @@
-
+
{device.name || `Device ${device.dev_eui}`}
-
+
{#if device.latestData}
{nameToEmoji(primaryDataKey)}{nameToEmoji(secondaryDataKey)}
- + {formatNumber({ key: primaryDataKey, value: primaryValue })} {primaryNotation} -
{#if secondaryDataKey} - +
{nameToEmoji(secondaryDataKey)} -
- - {formatNumber({ key: secondaryDataKey, value: secondaryValue })} - {secondaryNotation} +
+ + {formatNumber({ key: secondaryDataKey, value: secondaryValue })} + + {secondaryNotation} + -
{/if} diff --git a/src/lib/components/UI/form/UserPermissionsSelector.svelte b/src/lib/components/UI/form/UserPermissionsSelector.svelte index 538dd946..f56ddf3d 100644 --- a/src/lib/components/UI/form/UserPermissionsSelector.svelte +++ b/src/lib/components/UI/form/UserPermissionsSelector.svelte @@ -14,10 +14,11 @@ type Props = { data: { device?: DeviceWithType; ownerId: string }; ownerList: any[]; - canDelete: boolean; + canDelete?: boolean; }; let { data, ownerList, canDelete = false }: Props = $props(); + let device = $derived(data.device); let currentUserId = $derived(data.ownerId); let showingRemoveConfirmation = $state(false); @@ -87,7 +88,7 @@
{:else} {#each ownerList as owner (owner.id)} - {#if !owner.profile.email.includes('@cropwatch.io')} + {#if !owner.profile?.email?.includes('@cropwatch.io')}
diff --git a/src/lib/components/dashboard/DateRangeSelector.svelte b/src/lib/components/dashboard/DateRangeSelector.svelte index d18b1cdd..73399529 100644 --- a/src/lib/components/dashboard/DateRangeSelector.svelte +++ b/src/lib/components/dashboard/DateRangeSelector.svelte @@ -6,80 +6,175 @@ import { _ } from 'svelte-i18n'; type Props = { - startDateInputString: string; - endDateInputString: string; - handleDateRangeSubmit: (units?: number) => void; + startDateInput: Date; + endDateInput: Date; loadingHistoricalData: boolean; error?: string; + onDateChange: () => void; }; let { - startDateInputString = $bindable(), - endDateInputString = $bindable(), - handleDateRangeSubmit, - loadingHistoricalData, - error + startDateInput = $bindable(), + endDateInput = $bindable(), + loadingHistoricalData = false, + error, + onDateChange }: Props = $props(); + + // Helpers to format/parse dates for in LOCAL time + function toInputDateString(d: Date): string { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; + } + function parseInputDateLocal(value: string): Date | null { + if (!value) return null; + const [y, m, d] = value.split('-').map(Number); + if (!y || !m || !d) return null; + return new Date(y, m - 1, d); // local midnight + } + + // Ensure end defaults to end-of-today and start to 24h before end (only once at initial mount) + let __initialized = $state(false); + $effect(() => { + if (!__initialized) { + const end = new Date(); + end.setHours(23, 59, 59, 999); // end of current local day + endDateInput = end; + startDateInput = new Date(end.getTime() - 24 * 60 * 60 * 1000); + __initialized = true; + } + }); + + let startDateInputString = $derived(toInputDateString(startDateInput)); + let endDateInputString = $derived(toInputDateString(endDateInput)); + const todayString = $derived(toInputDateString(new Date())); + + const handleDateChange = () => { + if (startDateInput && endDateInput) { + // Clear any previous error + error = undefined; + // Call the parent's callback + onDateChange(); + } else { + error = 'Please select both start and end dates.'; + } + }; -
+
-
- -
- - -
-
- - -
-
- -
-
-
- -
-
- -
-
- {#if error} -

{'error'}

- {/if} + +
+ +
+ +
+ + { + const parsed = parseInputDateLocal(startDateInputString); + + if (!parsed) { + error = 'Invalid start date format.'; + return; + } + + // Normalize start to start-of-day + parsed.setHours(0, 0, 0, 0); + startDateInput = parsed; + + // If start date is after end date, align end to end-of-day of start + if (startDateInput > endDateInput) { + const end = new Date(startDateInput); + end.setHours(23, 59, 59, 999); + endDateInput = end; + } + + handleDateChange(); + }} + max={endDateInputString} + class="relative flex w-full rounded border border-gray-300 bg-white px-2 py-2 pr-10 text-sm text-xl text-gray-900 dark:border-zinc-700 dark:bg-zinc-800 dark:text-white" + /> +
+
+ + { + const parsed = parseInputDateLocal(endDateInputString); + if (!parsed) { + return; + } + // Normalize end to end-of-day + parsed.setHours(23, 59, 59, 999); + + // Cap to today end-of-day + const todayEnd = new Date(); + todayEnd.setHours(23, 59, 59, 999); + endDateInput = parsed > todayEnd ? todayEnd : parsed; + + // If end before start, align start to start-of-day of end + if (endDateInput < startDateInput) { + const start = new Date(endDateInput); + start.setHours(0, 0, 0, 0); + startDateInput = start; + } + + handleDateChange(); + }} + min={startDateInputString} + max={todayString} + class="relative flex w-full rounded border border-gray-300 bg-white px-2 py-2 pr-10 text-sm text-xl text-gray-900 dark:border-zinc-700 dark:bg-zinc-800 dark:text-white" + /> +
+ +
+
+ {#if error} +

{error}

+ {/if} +
diff --git a/src/lib/components/devices/ExportButton.svelte b/src/lib/components/devices/ExportButton.svelte index 8118602e..41c12066 100644 --- a/src/lib/components/devices/ExportButton.svelte +++ b/src/lib/components/devices/ExportButton.svelte @@ -2,7 +2,7 @@ import Button from '$lib/components/UI/buttons/Button.svelte'; import MaterialIcon from '$lib/components/UI/icons/MaterialIcon.svelte'; import type { ReportAlertPoint } from '$lib/models/Report'; - import { neutral } from '$lib/stores/toast.svelte'; + import { error, neutral, warning } from '$lib/stores/toast.svelte'; import { Dialog } from 'bits-ui'; import { _, locale as appLocale } from 'svelte-i18n'; @@ -25,7 +25,7 @@ buttonLabel, disabled = false, showDatePicker = true, - types = ['csv', 'pdf'], + types = ['csv'], // 'pdf'], startDateInputString = undefined, endDateInputString = undefined, alertPoints = [], @@ -67,6 +67,11 @@ if (!response.ok) { console.error(`Failed to download ${type} for device ${devEui}:`, response.statusText); + if (response.status === 404) { + warning('Report Not Found, Please create a reaport first.'); + } else { + error('Error Generating Report, contact support.'); + } return; } @@ -121,7 +126,7 @@ id="start-date" type="date" bind:value={startDate} - class="w-full rounded border border-gray-300 px-2 py-1" + class="relative w-full rounded border border-gray-300 px-2 py-1 pr-10" />
@@ -130,7 +135,7 @@ id="end-date" type="date" bind:value={endDate} - class="w-full rounded border border-gray-300 px-2 py-1" + class="relative w-full rounded border border-gray-300 px-2 py-1 pr-10" />
diff --git a/src/lib/components/global/Breadcrumbs.svelte b/src/lib/components/global/Breadcrumbs.svelte index 609b3b33..0e920fe0 100644 --- a/src/lib/components/global/Breadcrumbs.svelte +++ b/src/lib/components/global/Breadcrumbs.svelte @@ -1,6 +1,7 @@ + + {#snippet breadcrumb({ path, label, showArrow = true }: BreadcrumbProps)} {#if showArrow} diff --git a/src/lib/lorawan/dragino.ts b/src/lib/lorawan/dragino.ts index ffb46861..b656936d 100644 --- a/src/lib/lorawan/dragino.ts +++ b/src/lib/lorawan/dragino.ts @@ -1,6 +1,6 @@ export const DRAGINO_LT22222L_PAYLOADS = { - relay1On: 'AwER', // 030111 - relay1Off: 'AwAR', // 030011 - relay2On: 'AxEB', // 031101 - relay2Off: 'AxEA' // 031100 -}; \ No newline at end of file + relay1On: 'AwER', // 030111 + relay1Off: 'AwAR', // 030011 + relay2On: 'AxEB', // 031101 + relay2Off: 'AxEA' // 031100 +}; diff --git a/src/lib/pdf/pdfDataTable.ts b/src/lib/pdf/pdfDataTable.ts index ad9bffaf..c0e248c0 100644 --- a/src/lib/pdf/pdfDataTable.ts +++ b/src/lib/pdf/pdfDataTable.ts @@ -10,6 +10,7 @@ interface TableConfig { columnMargin: number; fontSize: number; headerHeight: number; + takeEvery: number; // This is the numbered item in the array to take and you will skip others unless they are an alert type. } const DEFAULT_CONFIG: TableConfig = { @@ -20,7 +21,8 @@ const DEFAULT_CONFIG: TableConfig = { cellHeight: 12, columnMargin: 10, fontSize: 7, - headerHeight: 15 + headerHeight: 15, + takeEvery: 3 }; /** @@ -44,6 +46,22 @@ export function createPDFDataTable({ }): void { const conf = { ...DEFAULT_CONFIG, ...config }; + // Apply takeEvery filtering (keep every Nth row) plus always keep rows containing any alert/warning cell (bgColor != white) + // N = conf.takeEvery (defaults to 3). If N <= 1, no sampling (all rows kept). + const samplingInterval = Math.max(1, conf.takeEvery || 1); + const workingRows = + samplingInterval > 1 + ? dataRows.filter((row, idx) => { + const onSeries = idx % samplingInterval === 0; // take first (idx 0), then every Nth + if (onSeries) return true; + // Include any row with an alert/warning (detected by any cell having a non-white bgColor) + const hasAlert = [row.header, ...row.cells].some( + (c) => c.bgColor && c.bgColor !== '#ffffff' + ); + return hasAlert; + }) + : dataRows; + const { caption, headerHeight, cellWidth, cellHeight, columnsPerPage, columnMargin } = conf; const { @@ -76,9 +94,9 @@ export function createPDFDataTable({ let dataIndex = 0; let startY = doc.y; - while (dataIndex < dataRows.length) { + while (dataIndex < workingRows.length) { // Draw columns for current page - for (let col = 0; col < finalColumnsPerPage && dataIndex < dataRows.length; col++) { + for (let col = 0; col < finalColumnsPerPage && dataIndex < workingRows.length; col++) { const firstColumn = col % finalColumnsPerPage === 0; if (firstColumn) { @@ -96,12 +114,12 @@ export function createPDFDataTable({ const actualRowsPerColumn = Math.floor(availableHeight / cellHeight); const startX = marginLeft + col * (columnWidth + columnMargin); - const endIndex = Math.min(dataIndex + actualRowsPerColumn, dataRows.length); + const endIndex = Math.min(dataIndex + actualRowsPerColumn, workingRows.length); drawColumn({ doc, dataHeader, - dataRows: dataRows.slice(dataIndex, endIndex), + dataRows: workingRows.slice(dataIndex, endIndex), columnWidth, startX, startY, diff --git a/src/lib/pdf/pdfLineChartImage.ts b/src/lib/pdf/pdfLineChartImage.ts index 7a3a9b1b..7e335743 100644 --- a/src/lib/pdf/pdfLineChartImage.ts +++ b/src/lib/pdf/pdfLineChartImage.ts @@ -1,5 +1,7 @@ import type { ReportAlertPoint } from '$lib/models/Report'; -import { createCanvas } from 'canvas'; +import { createCanvas, registerFont } from 'canvas'; +import path from 'path'; +import fs from 'fs'; import { CategoryScale, Chart, @@ -20,9 +22,30 @@ interface ChartConfig { options?: ChartOptions; } +// Attempt to register a bundled font (important for server environments like Vercel where system fonts are minimal) +(() => { + const candidatePaths = [ + path.join(process.cwd(), 'static/fonts/NotoSansJP-Regular.ttf'), + path.join(process.cwd(), 'src/lib/fonts/NotoSansJP-Regular.ttf'), + path.join(process.cwd(), 'server/fonts/NotoSansJP-Regular.ttf') + ]; + for (const p of candidatePaths) { + try { + if (fs.existsSync(p)) { + registerFont(p, { family: 'NotoSansJP' }); + Chart.defaults.font.family = 'NotoSansJP'; + break; + } + } catch { + /* ignore */ + } + } +})(); + Chart.register([CategoryScale, LineController, LineElement, LinearScale, PointElement]); Chart.defaults.devicePixelRatio = 3; Chart.defaults.font.size = 20; +Chart.defaults.font.family = Chart.defaults.font.family || 'sans-serif'; const DEFAULT_CHART_OPTIONS: ChartOptions = { elements: { diff --git a/src/lib/repositories/AirDataRepository.ts b/src/lib/repositories/AirDataRepository.ts deleted file mode 100644 index dc66085c..00000000 --- a/src/lib/repositories/AirDataRepository.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { SupabaseClient } from '@supabase/supabase-js'; -import { BaseRepository } from './BaseRepository'; -import type { AirData } from '../models/AirData'; -import { ErrorHandlingService } from '../errors/ErrorHandlingService'; - -/** - * Repository for air data access - */ -export class AirDataRepository extends BaseRepository { - protected tableName = 'cw_air_data'; - protected primaryKey = 'dev_eui'; - protected entityName = 'AirData'; - - /** - * Constructor with Supabase client dependency - */ - constructor(supabase: SupabaseClient, errorHandler: ErrorHandlingService) { - super(supabase, errorHandler); - } - - /** - * Find air data by device EUI - * @param devEui The device EUI - */ - async findByDeviceEui(devEui: string): Promise { - const { data, error } = await this.supabase - .from(this.tableName) - .select('*') - .eq('dev_eui', devEui) - .order('created_at', { ascending: false }); - - if (error) { - this.errorHandler.handleDatabaseError( - error, - `Error finding air data by device EUI: ${devEui}` - ); - } - - return (data as AirData[]) || []; - } - - /** - * Find latest air data for a device - * @param devEui The device EUI - */ - async findLatestByDeviceEui(devEui: string): Promise { - const { data, error } = await this.supabase - .from(this.tableName) - .select('*') - .eq('dev_eui', devEui) - .order('created_at', { ascending: false }) - .limit(1) - .single(); - - if (error) { - this.errorHandler.handleDatabaseError( - error, - `Error finding latest air data by device EUI: ${devEui}` - ); - } - - return data as AirData; - } - - /** - * Find air data by date range - * @param devEui The device EUI - * @param startDate The start date - * @param endDate The end date - */ - async findByDateRange(devEui: string, startDate: Date, endDate: Date): Promise { - const { data, error } = await this.supabase - .from(this.tableName) - .select('*') - .eq('dev_eui', devEui) - .gte('created_at', startDate.toISOString()) - .lte('created_at', endDate.toISOString()) - .order('created_at', { ascending: true }); - - if (error) { - this.errorHandler.handleDatabaseError( - error, - `Error finding air data by date range for device: ${devEui}` - ); - } - - return (data as AirData[]) || []; - } -} diff --git a/src/lib/repositories/DeviceRepository.ts b/src/lib/repositories/DeviceRepository.ts index b078d812..93c33e50 100644 --- a/src/lib/repositories/DeviceRepository.ts +++ b/src/lib/repositories/DeviceRepository.ts @@ -40,6 +40,7 @@ export class DeviceRepository extends BaseRepository { ` *, cw_device_type(*), + cw_device_owners(*, user_id), ip_log(*) ` ) diff --git a/src/lib/repositories/LocationRepository.ts b/src/lib/repositories/LocationRepository.ts index 57290f06..51a9796f 100644 --- a/src/lib/repositories/LocationRepository.ts +++ b/src/lib/repositories/LocationRepository.ts @@ -252,10 +252,26 @@ export class LocationRepository extends BaseRepository { userId: string, permissionLevel: number ): Promise { + // fetch the owner_id for this location + const { data: loc, error: locError } = await this.supabase + .from('cw_locations') + .select('owner_id') + .eq('location_id', locationId) + .single(); + + if (locError) { + this.errorHandler.handleDatabaseError( + locError, + `Error getting owner for location ${locationId}` + ); + return; + } + const { error } = await this.supabase.from('cw_location_owners').insert({ location_id: locationId, user_id: userId, permission_level: permissionLevel, + admin_user_id: loc.owner_id, // ensure this matches the location owner is_active: true }); diff --git a/src/lib/services/AirDataService.ts b/src/lib/services/AirDataService.ts deleted file mode 100644 index 5d402ce6..00000000 --- a/src/lib/services/AirDataService.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { IAirDataService } from '../interfaces/IAirDataService'; -import { AirDataRepository } from '../repositories/AirDataRepository'; -import type { AirData, AirDataInsert } from '../models/AirData'; - -/** - * Implementation of AirDataService - * This service handles all business logic related to air data - */ -export class AirDataService implements IAirDataService { - /** - * Constructor with AirDataRepository dependency - */ - constructor( - private airDataRepository: AirDataRepository - ) {} - - /** - * Get air data by device EUI - * @param devEui The device EUI - */ - async getAirDataByDevice(devEui: string): Promise { - return this.airDataRepository.findByDeviceEui(devEui); - } - - /** - * Get latest air data for a device - * @param devEui The device EUI - */ - async getLatestAirDataByDevice(devEui: string): Promise { - return this.airDataRepository.findLatestByDeviceEui(devEui); - } - - /** - * Get air data within a date range - * @param devEui The device EUI - * @param startDate The start date - * @param endDate The end date - */ - async getAirDataByDateRange(devEui: string, startDate: Date, endDate: Date): Promise { - return this.airDataRepository.findByDateRange(devEui, startDate, endDate); - } - - /** - * Create a new air data record - * @param airData The air data record to create - */ - async createAirData(airData: AirDataInsert): Promise { - return this.airDataRepository.create(airData); - } -} \ No newline at end of file diff --git a/src/lib/services/DeviceDataService.ts b/src/lib/services/DeviceDataService.ts index ac95a050..abca9df2 100644 --- a/src/lib/services/DeviceDataService.ts +++ b/src/lib/services/DeviceDataService.ts @@ -184,21 +184,50 @@ export class DeviceDataService implements IDeviceDataService { .eq('dev_eui', devEui) .gte(tableName == 'cw_traffic2' ? 'traffic_hour' : 'created_at', startDate.toISOString()) // SHIT FIX #1, traffic camera specific .lte(tableName == 'cw_traffic2' ? 'traffic_hour' : 'created_at', endDate.toISOString()) // SHIT FIX #2, traffic camera specific - .order(tableName == 'cw_traffic2' ? 'traffic_hour' : 'created_at', { ascending: false }) // SHIT FIX #3, traffic camera specific - .csv(); + .order(tableName == 'cw_traffic2' ? 'traffic_hour' : 'created_at', { ascending: false }); // SHIT FIX #3, traffic camera specific - // SHIT FIX #4, traffic camera specific - if (tableName == 'cw_traffic2') { - return (data || []).map((record: any) => ({ - ...record, - created_at: record.traffic_hour, - dev_eui: record.dev_eui, - note: 'Traffic data formatted' - })) as DeviceDataRecord[]; + if (error) { + this.errorHandler.logError(error); + throw new Error(`Error fetching CSV data: ${error.message}`); } - // END OF SHIT FIX - return (data || []) as DeviceDataRecord[]; + const rows = ((data || []) as Record[]).map((r) => ({ ...r })); + const timestampKey = tableName === 'cw_traffic2' ? 'traffic_hour' : 'created_at'; + + const normalizeToISO = (val: string) => { + let s = val.trim().replace(' ', 'T'); + s = s.replace(/([+-]\d{2})$/, '$1:00'); + s = s.replace(/([+-]\d{2})(\d{2})$/, '$1:$2'); + return s; + }; + + for (const row of rows) { + const raw = row[timestampKey]; + if (!raw) continue; + const isoLike = typeof raw === 'string' ? normalizeToISO(raw) : String(raw); + let dt = DateTime.fromISO(isoLike, { setZone: true }); + if (!dt.isValid) dt = DateTime.fromSQL(String(raw), { setZone: true }); + if (dt.isValid) { + // Format for Microsoft Excel compatibility: "yyyy-MM-dd HH:mm:ss" (no T, no timezone) + const excelLocal = dt.setZone('Asia/Tokyo').toFormat('yyyy-LL-dd HH:mm:ss'); + // Standardize on created_at in the CSV + row.created_at = excelLocal; + } + } + + // Build header order: dev_eui, created_at, then the rest in natural order + const headerSet = new Set(); + for (const row of rows) { + Object.keys(row).forEach((k) => headerSet.add(k)); + } + const headers = [ + 'dev_eui', + 'created_at', + ...Array.from(headerSet).filter((k) => k !== 'dev_eui' && k !== 'created_at') + ]; + + const csv = this.toCSV(rows, headers); + return csv as unknown as DeviceDataRecord[]; } catch (error) { // Handle errors with a generic response this.errorHandler.logError(error as Error); @@ -427,4 +456,31 @@ export class DeviceDataService implements IDeviceDataService { } return true; // Default to true for now } + + private toCSV(records: Record[], headers?: string[]): string { + if (!records || records.length === 0) { + return headers && headers.length ? headers.join(',') + '\n' : ''; + } + const cols = + headers && headers.length + ? headers + : Array.from(new Set(records.flatMap((r) => Object.keys(r)))); + + const escapeField = (val: any): string => { + if (val === null || val === undefined) return ''; + if (typeof val === 'boolean') return val ? 't' : 'f'; + const s = String(val); + const needsQuote = /[",\r\n]/.test(s); + const escaped = s.replace(/"/g, '""'); + return needsQuote ? `"${escaped}"` : escaped; + }; + + const lines: string[] = []; + lines.push(cols.join(',')); + for (const rec of records) { + const row = cols.map((c) => escapeField(rec[c])); + lines.push(row.join(',')); + } + return lines.join('\n'); + } } diff --git a/src/lib/services/DeviceService.ts b/src/lib/services/DeviceService.ts index e42f7884..8ef84c8b 100644 --- a/src/lib/services/DeviceService.ts +++ b/src/lib/services/DeviceService.ts @@ -8,74 +8,72 @@ import type { DeviceWithJoins } from '../repositories/DeviceRepository'; * This service handles all business logic related to devices */ export class DeviceService implements IDeviceService { - /** - * Constructor with DeviceRepository dependency - */ - constructor( - private deviceRepository: DeviceRepository - ) {} + /** + * Constructor with DeviceRepository dependency + */ + constructor(private deviceRepository: DeviceRepository) {} - /** - * Get a device by its EUI - * @param devEui The device EUI - */ - async getDeviceByEui(devEui: string): Promise { - return this.deviceRepository.findById(devEui); - } + /** + * Get a device by its EUI + * @param devEui The device EUI + */ + async getDeviceByEui(devEui: string): Promise { + return this.deviceRepository.findById(devEui); + } - /** - * Get a device with its type information - * @param devEui The device EUI - */ - async getDeviceWithTypeByEui(devEui: string): Promise { - return this.deviceRepository.getDeviceWithType(devEui); - } + /** + * Get a device with its type information + * @param devEui The device EUI + */ + async getDeviceWithTypeByEui(devEui: string): Promise { + return this.deviceRepository.getDeviceWithType(devEui); + } - /** - * Get all devices - */ - async getAllDevices(): Promise { - return this.deviceRepository.findAll(); - } + /** + * Get all devices + */ + async getAllDevices(): Promise { + return this.deviceRepository.findAll(); + } - /** - * Get devices by location ID - * @param locationId The location ID - */ - async getDevicesByLocation(locationId: number): Promise { - return this.deviceRepository.findByLocation(locationId); - } + /** + * Get devices by location ID + * @param locationId The location ID + */ + async getDevicesByLocation(locationId: number): Promise { + return this.deviceRepository.findByLocation(locationId); + } - /** - * Get devices by type ID - * @param typeId The device type ID - */ - async getDevicesByType(typeId: number): Promise { - return this.deviceRepository.findByType(typeId); - } + /** + * Get devices by type ID + * @param typeId The device type ID + */ + async getDevicesByType(typeId: number): Promise { + return this.deviceRepository.findByType(typeId); + } - /** - * Create a new device - * @param device The device to create - */ - async createDevice(device: DeviceInsert): Promise { - return this.deviceRepository.create(device); - } + /** + * Create a new device + * @param device The device to create + */ + async createDevice(device: DeviceInsert): Promise { + return this.deviceRepository.create(device); + } - /** - * Update an existing device - * @param devEui The device EUI - * @param device The device with updated values - */ - async updateDevice(devEui: string, device: DeviceUpdate): Promise { - return this.deviceRepository.update(devEui, device); - } + /** + * Update an existing device + * @param devEui The device EUI + * @param device The device with updated values + */ + async updateDevice(devEui: string, device: DeviceUpdate): Promise { + return this.deviceRepository.update(devEui, device); + } - /** - * Delete a device - * @param devEui The device EUI - */ - async deleteDevice(devEui: string): Promise { - return this.deviceRepository.delete(devEui); - } -} \ No newline at end of file + /** + * Delete a device + * @param devEui The device EUI + */ + async deleteDevice(devEui: string): Promise { + return this.deviceRepository.delete(devEui); + } +} diff --git a/src/lib/tests/services/AirDataService.test.ts b/src/lib/tests/services/AirDataService.test.ts deleted file mode 100644 index 4eeb5b8f..00000000 --- a/src/lib/tests/services/AirDataService.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { AirDataService } from '../../services/AirDataService'; -import { AirDataRepository } from '../../repositories/AirDataRepository'; -import { ErrorHandlingService } from '../../errors/ErrorHandlingService'; -import type { AirData } from '../../models/AirData'; -import type { AirDataDto } from '../../dtos/AirDataDto'; - -describe('AirDataService', () => { - let airDataService: AirDataService; - let mockRepository: any; - let errorHandlingService: ErrorHandlingService; - - const mockAirData: AirData[] = [ - { - dev_eui: 'abc123', - temperature: 25.5, - humidity: 65.2, - created_at: '2025-05-01T12:00:00Z', - }, - { - dev_eui: 'abc123', - temperature: 26.0, - humidity: 66.5, - created_at: '2025-05-01T13:00:00Z', - } - ]; - - beforeEach(() => { - mockRepository = { - findByDeviceEui: vi.fn(), - findLatestByDeviceEui: vi.fn(), - findByDateRange: vi.fn(), - create: vi.fn(), - update: vi.fn(), - delete: vi.fn() - }; - errorHandlingService = new ErrorHandlingService(); - airDataService = new AirDataService(mockRepository, errorHandlingService); - }); - - describe('getAirDataByDevice', () => { - it('should return air data for a device', async () => { - mockRepository.findByDeviceEui.mockResolvedValue(mockAirData); - - const result = await airDataService.getAirDataByDevice('abc123'); - - expect(mockRepository.findByDeviceEui).toHaveBeenCalledWith('abc123'); - expect(result).toHaveLength(2); - expect(result[0].temperature).toBe(25.5); - expect(result[1].humidity).toBe(66.5); - }); - - it('should handle errors when getting air data', async () => { - mockRepository.findByDeviceEui.mockRejectedValue(new Error('Database error')); - - await expect(airDataService.getAirDataByDevice('abc123')).rejects.toThrow('Database error'); - }); - }); - - describe('getLatestAirDataByDevice', () => { - it('should return latest air data for a device', async () => { - mockRepository.findLatestByDeviceEui.mockResolvedValue(mockAirData[1]); - - const result = await airDataService.getLatestAirDataByDevice('abc123'); - - expect(mockRepository.findLatestByDeviceEui).toHaveBeenCalledWith('abc123'); - expect(result).toBeDefined(); - expect(result.temperature).toBe(26.0); - }); - - it('should return null when no data exists', async () => { - mockRepository.findLatestByDeviceEui.mockResolvedValue(null); - - const result = await airDataService.getLatestAirDataByDevice('abc123'); - - expect(result).toBeNull(); - }); - }); - - describe('getAirDataByDateRange', () => { - it('should return air data within a date range', async () => { - mockRepository.findByDateRange.mockResolvedValue(mockAirData); - - const startDate = new Date('2025-05-01T00:00:00Z'); - const endDate = new Date('2025-05-02T00:00:00Z'); - - const result = await airDataService.getAirDataByDateRange('abc123', startDate, endDate); - - expect(mockRepository.findByDateRange).toHaveBeenCalledWith('abc123', startDate, endDate); - expect(result).toHaveLength(2); - }); - }); - - describe('create', () => { - it('should create new air data', async () => { - const airDataDto: AirDataDto = { - dev_eui: 'abc123', - temperature: 27.5, - humidity: 68.0 - }; - - const createdAirData: AirData = { - ...airDataDto, - created_at: '2025-05-01T14:00:00Z' - }; - - mockRepository.create.mockResolvedValue(createdAirData); - - const result = await airDataService.createAirData(airDataDto); - - expect(mockRepository.create).toHaveBeenCalledWith(airDataDto); - expect(result).toBeDefined(); - expect(result.temperature).toBe(27.5); - expect(result.created_at).toBeDefined(); - }); - }); -}); \ No newline at end of file diff --git a/src/lib/utilities/NameToEmoji.ts b/src/lib/utilities/NameToEmoji.ts index 108104b1..6b7baecc 100644 --- a/src/lib/utilities/NameToEmoji.ts +++ b/src/lib/utilities/NameToEmoji.ts @@ -1,74 +1,77 @@ export const nameToEmoji = (name: string) => { - switch (name) { - case 'soil_moisture': - case 'moisture': - case 'soil_humidity': - return '💧'; - case 'humidity': - return '💨'; - case 'dew_point': - case 'dew_pointC': - case 'dewPointC': - return '💦'; - case 'temperature': - case 'temperatureC': - case 'soil_temperatureC': - case 'temperature_c': - case 'soil_temperature': - return '🌡️'; - case 'soil_EC': - case 'soil_ec': - case 'ec': - return '🧂'; - case 'soil_N': - case 'soil_P': - case 'soil_K': - case 'soil_n': - case 'soil_p': - case 'soil_k': - return '🧪'; - case 'soil_PH': - case 'soil_ph': - case 'ph': - return '⚗️'; - case 'co2_level': - case 'co2': - return '⌬'; - case 'vpd': - return '💦'; - case 'rainfall': - return '🌧️'; - case 'pressure': - return '🕕'; - case 'created_at': - return '⌛'; - case 'wind_speed': - return '🍃'; - case 'lux': - return '☀️'; - case 'uv': - return '☢️'; - case 'wind_direction': - return '🧭'; - case 'water_level': - case 'depth_cm': - case 'deapth_cm': - return '📏'; - case 'battery_level': - case 'battery': - case 'Battery': - return '🔋'; - case 'sos': - return '🆘'; - case 'fire': - return '🔥'; - case 'people_count': - return '🧍'; - case 'car_count': - return '🚗'; - case 'bicycle_count': - return '🚲'; - default: - return ''; - } -} \ No newline at end of file + switch (name) { + case 'soil_moisture': + case 'moisture': + case 'soil_humidity': + return '💧'; + case 'humidity': + return '💨'; + case 'dew_point': + case 'dew_pointC': + case 'dewPointC': + return '💦'; + case 'temperature': + case 'temperatureC': + case 'soil_temperatureC': + case 'temperature_c': + case 'soil_temperature': + return '🌡️'; + case 'soil_EC': + case 'soil_ec': + case 'ec': + return '🧂'; + case 'soil_N': + case 'soil_P': + case 'soil_K': + case 'soil_n': + case 'soil_p': + case 'soil_k': + return '🧪'; + case 'soil_PH': + case 'soil_ph': + case 'ph': + return '⚗️'; + case 'co2_level': + case 'co2': + return '⌬'; + case 'vpd': + return '💦'; + case 'rainfall': + return '🌧️'; + case 'pressure': + return '🕕'; + case 'created_at': + return '⌛'; + case 'wind_speed': + return '🍃'; + case 'lux': + return '☀️'; + case 'uv': + return '☢️'; + case 'wind_direction': + return '🧭'; + case 'water_level': + case 'depth_cm': + case 'deapth_cm': + return '📏'; + case 'battery_level': + case 'battery': + case 'Battery': + return '🔋'; + case 'sos': + return '🆘'; + case 'fire': + return '🔥'; + case 'people_count': + return '🧍'; + case 'car_count': + return '🚗'; + case 'bicycle_count': + return '🚲'; + case 'relay_1': + case 'relay_2': + return '🔌'; + default: + return ''; + } +}; diff --git a/src/routes/api/devices/[devEui]/downlink/+server.ts b/src/routes/api/devices/[devEui]/downlink/+server.ts index b8ba4769..3b64b3c9 100644 --- a/src/routes/api/devices/[devEui]/downlink/+server.ts +++ b/src/routes/api/devices/[devEui]/downlink/+server.ts @@ -1,86 +1,148 @@ +// src/routes/api/v1/devices/[devEui]/downlink/+server.ts import { json, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { DeviceRepository } from '$lib/repositories/DeviceRepository'; import { DeviceService } from '$lib/services/DeviceService'; import { ErrorHandlingService } from '$lib/errors/ErrorHandlingService'; import { DRAGINO_LT22222L_PAYLOADS } from '$lib/lorawan/dragino'; -import { env } from '$env/dynamic/private'; +// SvelteKit v5: use $env/static/private on the server +import { PRIVATE_TTI_DRAGINO_RELAY_KEY } from '$env/static/private'; + +// --- Config --- +// IS (metadata) on EU1, AS (downlink) on AU1 unless you override via env. +const TTI_IS_BASE = 'https://cropwatch.eu1.cloud.thethings.industries'; +const TTI_AS_BASE = 'https://cropwatch.au1.cloud.thethings.industries'; +const TTI_KEY = PRIVATE_TTI_DRAGINO_RELAY_KEY; + +// Minimal fields to fetch when listing/searching devices +const FIELD_MASK = 'ids,ids.dev_eui,name'; + +// --- Helpers --- +function normDevEui(eui: string): string { + return eui.replace(/[:\-\s]/g, '').toUpperCase(); +} + +async function ttiFetch(url: string, init?: RequestInit): Promise { + if (!TTI_KEY) throw error(500, 'PRIVATE_TTI_DRAGINO_RELAY_KEY not configured'); + const headers = { + Authorization: `Bearer ${TTI_KEY}`, + 'Content-Type': 'application/json', + 'User-Agent': 'cropwatch-api/1.0', + ...(init?.headers ?? {}) + }; + return fetch(url, { ...init, headers }); +} + +type EndDeviceLite = { ids?: { device_id?: string; dev_eui?: string }; name?: string }; +type ListResp = { end_devices?: EndDeviceLite[] }; + +// Search (preferred). +async function resolveDeviceIdViaSearch(appId: string, devEui: string): Promise { + const url = new URL( + `${TTI_IS_BASE}/api/v3/search/applications/${encodeURIComponent(appId)}/devices` + ); + url.searchParams.set('page', '1'); + url.searchParams.set('limit', '200'); + url.searchParams.set('query', ''); + url.searchParams.set('order', '-created_at'); + url.searchParams.set('field_mask', FIELD_MASK); + + const r = await ttiFetch(url.toString()); + if (r.status === 404) return null; // search not exposed on some tenants + if (!r.ok) throw error(502, `TTI Search failed (${r.status}): ${await r.text()}`); + + const data = (await r.json()) as ListResp; + const match = (data.end_devices ?? []).find((d) => d?.ids?.dev_eui?.toUpperCase() === devEui); + return match?.ids?.device_id ?? null; +} + +// Fallback: List + client-side match (always available on EU1). +async function resolveDeviceIdViaList(appId: string, devEui: string): Promise { + const pageSize = 500; + for (let page = 1; page <= 200; page++) { + const url = new URL(`${TTI_IS_BASE}/api/v3/applications/${encodeURIComponent(appId)}/devices`); + url.searchParams.set('field_mask', FIELD_MASK); + url.searchParams.set('limit', String(pageSize)); + url.searchParams.set('page', String(page)); + + const r = await ttiFetch(url.toString()); + if (!r.ok) throw error(502, `TTI List failed (${r.status}): ${await r.text()}`); + + const data = (await r.json()) as ListResp; + const match = (data.end_devices ?? []).find((d) => d?.ids?.dev_eui?.toUpperCase() === devEui); + if (match?.ids?.device_id) return match.ids.device_id; + + if ((data.end_devices?.length ?? 0) < pageSize) break; + } + return null; +} + +async function resolveTtiDeviceId(appId: string, devEuiRaw: string): Promise { + const eui = normDevEui(devEuiRaw); + const fromSearch = await resolveDeviceIdViaSearch(appId, eui); + if (fromSearch) return fromSearch; + + const fromList = await resolveDeviceIdViaList(appId, eui); + if (fromList) return fromList; + + throw error(404, 'TTI device not found for DevEUI in this application'); +} + +// --- Route --- export const POST: RequestHandler = async ({ params, request, locals: { supabase, safeGetSession } }) => { - const { devEui } = params; const { session, user } = await safeGetSession(); - if (!session || !user) { - throw error(401, 'Authentication required'); - } + if (!session || !user) throw error(401, 'Authentication required'); - if (!devEui) { - throw error(400, 'Device EUI is required'); - } + const devEuiParam = params.devEui; + if (!devEuiParam) throw error(400, 'Device EUI is required'); const errorHandler = new ErrorHandlingService(); const repo = new DeviceRepository(supabase, errorHandler); const deviceService = new DeviceService(repo); - const device = await deviceService.getDeviceWithTypeByEui(devEui); - if (!device) { - throw error(404, 'Device not found'); - } + const device = await deviceService.getDeviceWithTypeByEui(devEuiParam); + if (!device) throw error(404, 'Device not found'); - const owner = await repo.findDeviceOwner(devEui, user.id); - if (!owner) { - throw error(403, 'Forbidden'); - } + const owner = await repo.findDeviceOwner(devEuiParam, user.id); + if (!owner) throw error(403, 'Forbidden'); - const body = await request.json(); + const body = await request.json().catch(() => ({}) as any); const payloadName = body.payloadName as keyof typeof DRAGINO_LT22222L_PAYLOADS | undefined; const frm_payload = body.frm_payload as string | undefined; - const base64Payload = payloadName ? DRAGINO_LT22222L_PAYLOADS[payloadName] : frm_payload; - if (!base64Payload) { - throw error(400, 'No payload specified'); - } + if (!base64Payload) throw error(400, 'No payload specified'); const appId = device.cw_device_type?.TTI_application_id; - if (!appId) { - throw error(500, 'Device type missing TTI application id'); - } + if (!appId) throw error(500, 'Device type missing TTI application id'); - const apiKey = env.TTI_API_KEY; - if (!apiKey) { - throw error(500, 'TTI_API_KEY not configured'); - } + const ttiDeviceId = device.tti_name || (await resolveTtiDeviceId(appId, devEuiParam)); - const url = `https://cropwatch.au1.cloud.thethings.industries/api/v3/as/applications/${appId}/devices/${device.tti_name}/down/replace`; - - const payload = { - downlinks: [ - { - frm_payload: base64Payload, - f_port: 2, - priority: 'HIGH', - confirmed: true - } - ] + const downlink = { + frm_payload: base64Payload, + f_port: Number.isInteger(body.f_port) ? body.f_port : 2, + priority: (body.priority as 'LOW' | 'NORMAL' | 'HIGH' | 'HIGHEST') ?? 'HIGH', + confirmed: typeof body.confirmed === 'boolean' ? body.confirmed : true }; - const resp = await fetch(url, { + const url = `${TTI_AS_BASE}/api/v3/as/applications/${encodeURIComponent( + appId + )}/devices/${encodeURIComponent(ttiDeviceId)}/down/replace`; + + const resp = await ttiFetch(url, { method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${apiKey}` - }, - body: JSON.stringify(payload) + body: JSON.stringify({ downlinks: [downlink] }) }); if (!resp.ok) { const text = await resp.text(); console.error('TTI downlink error', text); - throw error(500, 'Failed to send downlink'); + throw error(502, `Failed to send downlink (${resp.status})`); } - return json({ success: true }); + return json({ success: true, appId, device_id: ttiDeviceId }); }; diff --git a/src/routes/api/devices/[devEui]/pdf/+server.ts b/src/routes/api/devices/[devEui]/pdf/+server.ts index 69e7fbc1..db3fd887 100644 --- a/src/routes/api/devices/[devEui]/pdf/+server.ts +++ b/src/routes/api/devices/[devEui]/pdf/+server.ts @@ -99,7 +99,7 @@ export const GET: RequestHandler = async ({ params, url, locals: { supabase } }) console.error('Error fetching report parameters:', error.message); return json( { error: `Failed to fetch report parameters - ${error.message}` }, - { status: 500 } + { status: 404 } ); } @@ -286,12 +286,12 @@ export const GET: RequestHandler = async ({ params, url, locals: { supabase } }) break; } } catch { - //console.log(`Could not load font from: ${fontPath}`); + console.error(`Could not load font from: ${fontPath}`); } } if (!fontLoaded) { - //console.log('Using default font - Japanese characters may not display correctly'); + console.error('Using default font - Japanese characters may not display correctly'); } // Professional header with Japanese styling @@ -341,7 +341,7 @@ export const GET: RequestHandler = async ({ params, url, locals: { supabase } }) doc.x = 400; doc - .text(`${$_('date_range')}: ${startDateParam} – ${endDateParam}`) + .text(`${$_('date_range')}: ${startDateParam} - ${endDateParam}`) .text(`${$_('sampling_size')}: ${deviceData.length}`); doc.x = marginLeft; @@ -353,6 +353,11 @@ export const GET: RequestHandler = async ({ params, url, locals: { supabase } }) ? numericKeys : numericKeys.filter((key) => selectedKeys.includes(key)); + // Only show columns in the final data table that participate in at least one alert + const alertKeySet = new Set(alertPoints.map((p) => p.data_point_key)); + const alertKeys = validKeys.filter((k) => alertKeySet.has(k)); + const tableKeys = alertKeys.length ? alertKeys : validKeys; // fallback if no alerts + const keyColumns: TableCell[] = validKeys.map((key) => ({ label: $_(key), value: '', @@ -405,6 +410,17 @@ export const GET: RequestHandler = async ({ params, url, locals: { supabase } }) // Prepare data rows for the table const dataRows = getDataRows(); + const tableKeyColumns: TableCell[] = tableKeys.map((key) => ({ + label: $_(key), + value: '', + width: 30, + color: getColorNameByKey(key) + })); + const dataHeaderTable: TableRow = { + header: { label: $_('datetime'), value: '', width: 40 }, + cells: [...tableKeyColumns, { label: $_('comment'), width: 40 }] + }; + const dataRowsTable = getDataRows(tableKeys); // Summary table createPDFDataTable({ @@ -455,8 +471,9 @@ export const GET: RequestHandler = async ({ params, url, locals: { supabase } }) const chartWidth = contentWidth; const chartHeight = contentWidth * 0.4; - // Charts - for (const key of validKeys) { + // Charts (restricted to the same alert-linked columns used for the data table) + const chartKeys = tableKeys; // tableKeys already falls back to validKeys if no alerts + for (const key of chartKeys) { // Add a new page if the content exceeds the current page height if (doc.y > doc.page.height - marginBottom - chartHeight + 20) { doc.addPage(); @@ -487,10 +504,17 @@ export const GET: RequestHandler = async ({ params, url, locals: { supabase } }) }); } - doc.moveDown(2); + // After the last chart, force a page break before the full data table for clarity + if (chartKeys.length > 0) { + doc.addPage(); + } + // Position after page break (or just add spacing if no charts) + if (chartKeys.length === 0) { + doc.moveDown(2); + } - // Full data table - createPDFDataTable({ doc, dataHeader, dataRows }); + // Full data table (restricted to alert-linked columns) + createPDFDataTable({ doc, dataHeader: dataHeaderTable, dataRows: dataRowsTable }); const footerText = [ location.name, diff --git a/src/routes/api/locations/[locationId]/devices/+server.ts b/src/routes/api/locations/[locationId]/devices/+server.ts index d2d44052..c4a53fe2 100644 --- a/src/routes/api/locations/[locationId]/devices/+server.ts +++ b/src/routes/api/locations/[locationId]/devices/+server.ts @@ -3,9 +3,7 @@ import type { RequestHandler } from './$types'; import type { DeviceType } from '$lib/models/Device'; import { ErrorHandlingService } from '$lib/errors/ErrorHandlingService'; import { DeviceRepository } from '$lib/repositories/DeviceRepository'; -import { AirDataRepository } from '$lib/repositories/AirDataRepository'; import { DeviceService } from '$lib/services/DeviceService'; -import { AirDataService } from '$lib/services/AirDataService'; import { DeviceDataService } from '$lib/services/DeviceDataService'; export const GET: RequestHandler = async ({ params, locals }) => { @@ -21,11 +19,10 @@ export const GET: RequestHandler = async ({ params, locals }) => { // Create repositories with per-request Supabase client const deviceRepo = new DeviceRepository(locals.supabase, errorHandler); - const airDataRepo = new AirDataRepository(locals.supabase, errorHandler); + const dataService = new DeviceDataService(locals.supabase, errorHandler); // Create services with repositories const deviceService = new DeviceService(deviceRepo); - const airDataService = new AirDataService(airDataRepo); const deviceDataService = new DeviceDataService(locals.supabase); // Get devices for this location - now includes device type info directly @@ -57,7 +54,7 @@ export const GET: RequestHandler = async ({ params, locals }) => { // This maintains backward compatibility if (!latestData) { // Get latest air data for this device, if available - const latestAirData = await airDataService.getLatestAirDataByDevice(device.dev_eui); + const latestAirData = await deviceDataService.getLatestDeviceData(device.dev_eui); // Set latestData to whichever data is available latestData = latestAirData || null; diff --git a/src/routes/app/account-settings/general/+page.server.ts b/src/routes/app/account-settings/general/+page.server.ts index af0d8f36..8ff48bf5 100644 --- a/src/routes/app/account-settings/general/+page.server.ts +++ b/src/routes/app/account-settings/general/+page.server.ts @@ -7,7 +7,7 @@ export const load: PageServerLoad = async ({ locals: { supabase, safeGetSession } const { data: profile } = await supabase .from('profiles') - .select(`username, full_name, website, avatar_url`) + .select(`username, full_name, website, avatar_url, line_id`) .eq('id', session.user.id) .single(); return { session, profile }; @@ -43,6 +43,15 @@ export const actions: Actions = { avatarUrl }; }, + disconnectLine: async ({ locals: { supabase, safeGetSession } }) => { + const { session } = await safeGetSession(); + if (!session) redirect(303, '/'); + await supabase + .from('profiles') + .update({ line_id: null, updated_at: new Date() }) + .eq('id', session.user.id); + return { lineDisconnected: true }; + }, signout: async ({ locals: { supabase, safeGetSession } }) => { const { session } = await safeGetSession(); if (session) { diff --git a/src/routes/app/account-settings/general/+page.svelte b/src/routes/app/account-settings/general/+page.svelte index 6287a8c2..1c1f6c9c 100644 --- a/src/routes/app/account-settings/general/+page.svelte +++ b/src/routes/app/account-settings/general/+page.svelte @@ -26,6 +26,12 @@ update(); }; }; + function startLineConnect() { + const params = new URLSearchParams({ + redirect: window.location.origin + '/app/account-settings/general/line/callback' + }); + window.location.href = '/app/account-settings/general/line/connect?' + params.toString(); + } @@ -167,6 +173,50 @@
+ + +
+

+ LINE + {#if profile?.line_id} + {$_('Connected')} + {:else} + {$_('Not Connected')} + {/if} +

+

+ {$_('Connect your LINE account to receive messages and notifications.')} +

+ {#if profile?.line_id} +
+ +
+
+ {$_('LINE User ID')}: {profile.line_id} +
+ {:else} + + {/if} +
diff --git a/src/routes/app/account-settings/general/line/callback/+server.ts b/src/routes/app/account-settings/general/line/callback/+server.ts new file mode 100644 index 00000000..bb9e1503 --- /dev/null +++ b/src/routes/app/account-settings/general/line/callback/+server.ts @@ -0,0 +1,97 @@ +import { redirect, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { PUBLIC_LINE_CHANNEL_ID } from '$env/static/public'; +import { LINE_CHANNEL_SECRET } from '$env/static/private'; + +interface LineTokenResponse { + access_token: string; + expires_in: number; + id_token: string; + refresh_token?: string; + scope: string; + token_type: string; +} +interface LineIdTokenPayload { + sub: string; // LINE user ID + aud: string; + iss: string; + exp: number; + iat: number; + name?: string; + picture?: string; +} + +export const GET: RequestHandler = async ({ + url, + cookies, + locals: { supabase, safeGetSession } +}) => { + const { session } = await safeGetSession(); + if (!session) redirect(303, '/auth/login'); + + const code = url.searchParams.get('code'); + const state = url.searchParams.get('state'); + const err = url.searchParams.get('error'); + if (err) + return redirect(303, `/app/account-settings/general?line_error=${encodeURIComponent(err)}`); + if (!code || !state) throw error(400, 'Missing code/state'); + + const storedState = cookies.get('line_oauth_state'); + const codeVerifier = cookies.get('line_oauth_verifier'); + if (!storedState || storedState !== state || !codeVerifier) throw error(400, 'State mismatch'); + + // Clear cookies + cookies.delete('line_oauth_state', { path: '/' }); + cookies.delete('line_oauth_verifier', { path: '/' }); + + const clientId = PUBLIC_LINE_CHANNEL_ID; + const clientSecret = LINE_CHANNEL_SECRET; + if (!clientId || !clientSecret) throw error(500, 'LINE env missing'); + + const redirectUri = `${url.origin}/app/account-settings/general/line/callback`; + + const tokenRes = await fetch('https://api.line.me/oauth2/v2.1/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'authorization_code', + code, + redirect_uri: redirectUri, + client_id: clientId, + client_secret: clientSecret, + code_verifier: codeVerifier + }) + }); + if (!tokenRes.ok) { + const text = await tokenRes.text(); + return redirect( + 303, + `/app/account-settings/general?line_error=token_exchange_failed&detail=${encodeURIComponent(text)}` + ); + } + const tokenJson = (await tokenRes.json()) as LineTokenResponse; + if (!tokenJson.id_token) + return redirect(303, '/app/account-settings/general?line_error=missing_id_token'); + + // Decode (no signature verify yet) + const [, payloadB64] = tokenJson.id_token.split('.'); + if (!payloadB64) return redirect(303, '/app/account-settings/general?line_error=bad_id_token'); + const payloadJson = JSON.parse( + Buffer.from(payloadB64.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf-8') + ) as LineIdTokenPayload; + const lineUserId = payloadJson.sub; + if (!lineUserId) + return redirect(303, '/app/account-settings/general?line_error=missing_line_user'); + + const { error: upErr } = await supabase + .from('profiles') + .update({ line_id: lineUserId, updated_at: new Date() }) + .eq('id', session.user.id); + if (upErr) + return redirect( + 303, + `/app/account-settings/general?line_error=save_failed&detail=${encodeURIComponent(upErr.message)}` + ); + + return redirect(303, '/app/account-settings/general?line_connected=1'); +}; diff --git a/src/routes/app/account-settings/general/line/connect/+server.ts b/src/routes/app/account-settings/general/line/connect/+server.ts new file mode 100644 index 00000000..2e2398ab --- /dev/null +++ b/src/routes/app/account-settings/general/line/connect/+server.ts @@ -0,0 +1,59 @@ +import { redirect, error } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { PUBLIC_LINE_CHANNEL_ID } from '$env/static/public'; + +// Utility: base64url encode ArrayBuffer +function b64url(buffer: ArrayBuffer) { + return Buffer.from(buffer) + .toString('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); +} +// PKCE code verifier (43-128 chars) +function generateCodeVerifier() { + const bytes = new Uint8Array(32); + crypto.getRandomValues(bytes); + return b64url(bytes.buffer); // already url-safe +} + +export const GET: RequestHandler = async ({ url, cookies, locals: { safeGetSession } }) => { + const { session } = await safeGetSession(); + if (!session) redirect(303, '/auth/login'); + + const clientId = PUBLIC_LINE_CHANNEL_ID; + if (!clientId) throw error(500, 'Missing PUBLIC_LINE_CHANNEL_ID'); + + const state = crypto.randomUUID(); + const codeVerifier = generateCodeVerifier(); + const challengeBuffer = await crypto.subtle.digest( + 'SHA-256', + new TextEncoder().encode(codeVerifier) + ); + const codeChallenge = b64url(challengeBuffer); + + // Persist values in httpOnly cookies (10 min lifetime) + const cookieOpts = { + path: '/', + httpOnly: true, + sameSite: 'lax' as const, + secure: url.protocol === 'https:', + maxAge: 600 + }; + cookies.set('line_oauth_state', state, cookieOpts); + cookies.set('line_oauth_verifier', codeVerifier, cookieOpts); + + const redirectUri = `${url.origin}/app/account-settings/general/line/callback`; + + const authUrl = new URL('https://access.line.me/oauth2/v2.1/authorize'); + authUrl.searchParams.set('response_type', 'code'); + authUrl.searchParams.set('client_id', clientId); + authUrl.searchParams.set('redirect_uri', redirectUri); + authUrl.searchParams.set('state', state); + authUrl.searchParams.set('scope', 'profile openid'); + authUrl.searchParams.set('prompt', 'consent'); + authUrl.searchParams.set('code_challenge', codeChallenge); + authUrl.searchParams.set('code_challenge_method', 'S256'); + + return redirect(302, authUrl.toString()); +}; diff --git a/src/routes/app/dashboard/+page.svelte b/src/routes/app/dashboard/+page.svelte index 671a5dbf..832cfd4c 100644 --- a/src/routes/app/dashboard/+page.svelte +++ b/src/routes/app/dashboard/+page.svelte @@ -9,6 +9,9 @@ // Get user data from the server load function let { data } = $props(); const user = data.user; + let isTabVisible = $state(true); + let lastRefresh = $state(new Date()); + // Extended Location type to include cw_devices property interface LocationWithDevices extends Location { cw_devices?: DeviceWithSensorData[]; @@ -70,6 +73,34 @@ } }); + $effect(() => { + function handleVisibilityChange() { + isTabVisible = document.visibilityState === 'visible'; + if (isTabVisible) { + if (browser) { + console.log('Tab is visible - refreshing data'); + const savedState = localStorage.getItem('sidebar_collapsed'); + if (savedState !== null) { + sidebarCollapsed = savedState === 'true'; + } + setupRealtimeSubscription(); + } + } else { + console.log('Tab is not visible'); + data.supabase.removeAllChannels(); + cleanupTimers(); + cleanupRealtimeSubscription(); + if (channel) { + data.supabase.realtime.removeChannel(channel); + } + } + } + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => { + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }); + // Initialize the dashboard UI store for preferences const uiStore = getDashboardUIStore(); @@ -171,6 +202,8 @@ onDestroy(() => { console.log('the component is being destroyed'); data.supabase.removeAllChannels(); + cleanupTimers(); + cleanupRealtimeSubscription(); if (channel) { data.supabase.realtime.removeChannel(channel); } @@ -186,59 +219,6 @@ } }); - // All polling and timer state is now managed by the DeviceTimerManager - - // Clean up timers and subscriptions when component is destroyed - onDestroy(() => { - //console.log('Cleaning up dashboard resources'); - cleanupTimers(); - cleanupRealtimeSubscription(); - }); - - // // Refresh session if needed - // async function refreshSessionIfNeeded() { - // try { - // const { data: sessionData, error: sessionError } = await data.supabase.auth.getSession(); - - // if (sessionError) { - // console.error('❌ Error getting session:', sessionError); - // return false; - // } - - // if (!sessionData.session) { - // console.error('❌ No session found'); - // return false; - // } - - // const expiresAt = sessionData.session.expires_at; - // if (expiresAt) { - // const expirationDate = new Date(expiresAt * 1000); - // const now = new Date(); - // const fiveMinutesFromNow = new Date(now.getTime() + 5 * 60 * 1000); - - // // If expired or expiring soon, refresh - // if (expirationDate < fiveMinutesFromNow) { - // console.log('🔄 Session expiring soon, refreshing...'); - // const { data: refreshData, error: refreshError } = - // await data.supabase.auth.refreshSession(); - - // if (refreshError) { - // console.error('❌ Failed to refresh session:', refreshError); - // return false; - // } - - // console.log('✅ Session refreshed successfully'); - // return true; - // } - // } - - // return true; - // } catch (error) { - // console.error('❌ Error refreshing session:', error); - // return false; - // } - // } - // Initialize dashboard on mount // This is the main onMount function for the dashboard onMount(async () => { @@ -324,39 +304,39 @@ } // Function to select a location and load its devices - async function selectLocation(locationId: number | null) { - //console.log('Dashboard selectLocation called with:', locationId); - //console.log('Current selectedLocationId:', locationsStore.selectedLocationId); - - // If already selected, do nothing - if (locationsStore.selectedLocationId === locationId) { - //console.log('Location already selected, returning'); - return; - } - - // Clean up any existing polling before changing location - timerManager.cleanupPolling(); + // async function selectLocation(locationId: number | null) { + // //console.log('Dashboard selectLocation called with:', locationId); + // //console.log('Current selectedLocationId:', locationsStore.selectedLocationId); + + // // If already selected, do nothing + // if (locationsStore.selectedLocationId === locationId) { + // //console.log('Location already selected, returning'); + // return; + // } - // Use the store to select location and load devices - //console.log('Calling store.selectLocation with:', locationId); - await locationsStore.selectLocation(locationId); - //console.log( - // 'After store.selectLocation, selectedLocationId is:', - // locationsStore.selectedLocationId - // ); - - // Setup active timers for each device - locationsStore.devices.forEach((device: DeviceWithSensorData) => { - if (device.latestData?.created_at) { - setupDeviceActiveTimer(device, timerManager, deviceActiveStatus); - } - }); + // // Clean up any existing polling before changing location + // timerManager.cleanupPolling(); + + // // Use the store to select location and load devices + // //console.log('Calling store.selectLocation with:', locationId); + // await locationsStore.selectLocation(locationId); + // //console.log( + // // 'After store.selectLocation, selectedLocationId is:', + // // locationsStore.selectedLocationId + // // ); + + // // Setup active timers for each device + // locationsStore.devices.forEach((device: DeviceWithSensorData) => { + // if (device.latestData?.created_at) { + // setupDeviceActiveTimer(device, timerManager, deviceActiveStatus); + // } + // }); - // Set up polling only for specific locations, not for "All Locations" - if (locationId !== null) { - timerManager.setupPolling(locationId, refreshDevicesForLocation); - } - } + // // Set up polling only for specific locations, not for "All Locations" + // if (locationId !== null) { + // timerManager.setupPolling(locationId, refreshDevicesForLocation); + // } + // } // Note: handleKeyDown is now handled in the LocationSidebar component diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/+page.svelte b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/+page.svelte index 3b913a7e..56e33d9f 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/+page.svelte +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/+page.svelte @@ -25,6 +25,7 @@ import { getDeviceDetailDerived, setupDeviceDetail } from './device-detail.svelte'; import Header from './Header.svelte'; import { setupRealtimeSubscription } from './realtime.svelte'; + import RelayControl from '$lib/components/RelayControl.svelte'; // Get device data from server load function let { data }: PageProps = $props(); @@ -35,6 +36,11 @@ let dataType = $state(data.dataType); let latestData: DeviceDataRecord | null = $state(null); let historicalData: DeviceDataRecord[] = $state([]); + let userId = $state(data.user.id); // User ID for permissions + let devicePermissionLevelState = $state(data.device.cw_device_owners); + let devicePermissionLevel = $derived( + devicePermissionLevelState.find((owner) => owner.user_id === userId)?.permission_level || null + ); // Define the type for a calendar event interface CalendarEvent { @@ -69,9 +75,11 @@ let mainChartElements: HTMLElement[] = $state([]); // Holds chart elements for rendering let blushChartElements: HTMLElement[] = $state([]); // Holds brush elements for rendering - // Local string states for date inputs, bound to the input fields - let startDateInputString = $state(''); - let endDateInputString = $state(''); + // Date range selection - initialize in LOCAL time (end: today 23:59:59.999, start: end - 24h) + const __endInit = new Date(); + __endInit.setHours(23, 59, 59, 999); + let endDateInput = $state(__endInit); + let startDateInput = $state(new Date(__endInit.getTime() - 24 * 60 * 60 * 1000)); let loadingHistoricalData = $state(false); // Local loading state for historical data fetch let renderingVisualization = $state(false); // Local rendering state for visualization @@ -88,17 +96,8 @@ let channel: RealtimeChannel | undefined = $state(undefined); // Channel for realtime updates onMount(() => { + // Initialize the device detail date range (this might be used internally by deviceDetail) initializeDateRange(); // This sets deviceDetail.startDate and deviceDetail.endDate (Date objects) - - // Sync input strings with initial Date objects from deviceDetail - startDateInputString = formatDateForInput(deviceDetail.startDate); - endDateInputString = formatDateForInput(deviceDetail.endDate); - }); - - $effect(() => { - (async () => { - latestData = await data.latestData; - })(); }); $effect(() => { @@ -129,6 +128,31 @@ })(); }); + $effect(() => { + // Initialize latestData from the streamed promise once + if (latestData === null && data.latestData) { + (async () => { + try { + const _latest = await data.latestData; + if (_latest) { + latestData = _latest as DeviceDataRecord; + } else if (historicalData.length) { + // Fallback: use most recent historical record if available + latestData = historicalData[historicalData.length - 1]; + } + } catch (e) { + console.error('Failed to resolve latestData promise:', e); + } + })(); + } + }); + + $effect(() => { + if (latestData === null && historicalData.length) { + latestData = historicalData[historicalData.length - 1]; + } + }); + const renderChart = async () => { if (renderingVisualization || !numericKeys.length) { return; @@ -165,18 +189,6 @@ calendarEvents = updateEvents(historicalData); }); - // Effect to keep input strings in sync if deviceDetail.startDate/endDate change (e.g. by initializeDateRange) - $effect(() => { - if (deviceDetail.startDate) { - startDateInputString = formatDateForInput(deviceDetail.startDate); - } - }); - $effect(() => { - if (deviceDetail.endDate) { - endDateInputString = formatDateForInput(deviceDetail.endDate); - } - }); - const updateEvents = (data: any[] = historicalData): CalendarEvent[] => { // Group data by date const dailyStats: { [date: string]: Record } = {}; @@ -233,59 +245,30 @@ }; // Function to handle fetching data for a specific date range - async function handleDateRangeSubmit(units?: number) { - if (!startDateInputString || !endDateInputString) { + async function handleDateRangeSubmit() { + if (!startDateInput || !endDateInput) { deviceDetail.error = 'Please select both start and end dates.'; return; } - // Parse local string dates from input into Date objects - // new Date('YYYY-MM-DD') can have timezone issues. Parsing components is safer. - const [sYear, sMonth, sDay] = startDateInputString.split('-').map(Number); - const finalStartDate = new Date(sYear, sMonth - 1, sDay); // Month is 0-indexed - - const [eYear, eMonth, eDay] = endDateInputString.split('-').map(Number); - // Set end date to the end of the selected day to be inclusive for the query - const finalEndDate = new Date(eYear, eMonth - 1, eDay, 23, 59, 59, 999); // Month is 0-indexed - - if (finalStartDate > finalEndDate) { - deviceDetail.error = 'Start date must be before end date.'; - return; - } - loadingHistoricalData = true; - deviceDetail.error = null; // Clear previous errors before fetching - - // If units is provided, slide the date range forward or backward - if (units !== undefined) { - //get range between start and end dates - let endDateTime = DateTime.fromJSDate(finalEndDate); - let startDateTime = DateTime.fromJSDate(finalStartDate); - const diffInDays = Math.round(Math.abs(startDateTime.diff(endDateTime, ['days']).days)); - if (units < 0) { - // Slide back - startDateTime = startDateTime.minus({ days: diffInDays }); - endDateTime = endDateTime.minus({ days: diffInDays }); - } else if (units > 0) { - // Slide forward - startDateTime.plus({ days: diffInDays }).toJSDate(); - endDateTime.plus({ days: diffInDays }).toJSDate(); - } - // update input strings to reflect the new range - startDateInputString = formatDateForInput(startDateTime.toJSDate()); - endDateInputString = formatDateForInput(endDateTime.toJSDate()); - } - const newData = await fetchDataForDateRange(device, finalStartDate, finalEndDate); - //console.log('Requested range:', finalStartDate, finalEndDate, 'Received:', newData); - if (newData) { - historicalData = newData; // This will trigger $effect for renderVisualization - calendarEvents = updateEvents(newData); // Use newData directly - } else { - calendarEvents = updateEvents(historicalData); + try { + const newData = await fetchDataForDateRange(device, startDateInput, endDateInput); + if (newData) { + historicalData = newData; // Set the historical data + processHistoricalData(newData); // Process the new data to update stats and chartData + numericKeys = getNumericKeys(newData); // Update numeric keys for the new data + calendarEvents = updateEvents(newData); // Use newData directly + } else { + calendarEvents = updateEvents(historicalData); + } + } catch (error) { + console.error('Error fetching date range data:', error); + deviceDetail.error = 'Failed to fetch data for the selected date range.'; + } finally { + loadingHistoricalData = false; } - - loadingHistoricalData = false; } // Expose formatDateForDisplay for the template, aliased from helpers @@ -298,175 +281,164 @@
- {#if numericKeys.length} - + {#if (numericKeys.length && device.user_id == userId) || devicePermissionLevel <= 2} + + {/if} + + {#if device.user_id == userId || devicePermissionLevel === 1} + {/if} -
-
- -
- -
-

{$_('Latest Sensor Readings')}

- {#if latestData} -
-
- {#each Object.keys(latestData) as key} - {#if !['id', 'dev_eui', 'created_at', 'is_simulated', 'battery_level', 'vape_detected', 'smoke_detected', 'traffic_hour'].includes(key) && latestData[key] !== null} - - {/if} - {/each} -
- -

- {$_('Last updated:')} - {formatDateForDisplay(latestData.created_at)} -

-
- {:else} -

{$_('No recent data available')}

- {/if} -
+ +
+
+ +
+ +
+

{$_('Latest Sensor Readings')}

+ {#if latestData} +
+
+ {#each Object.keys(latestData) as key} + {#if !['id', 'dev_eui', 'created_at', 'is_simulated', 'battery_level', 'vape_detected', 'smoke_detected', 'traffic_hour'].includes(key) && latestData[key] !== null} + + {/if} + {/each} +
- -
+

+ {$_('Last updated:')} + {formatDateForDisplay(latestData.created_at)} +

+
+ {:else} +

{$_('No recent data available')}

+ {/if} + - -
- - {#if loadingHistoricalData} -
- - {$_('Loading historical data...')} -
- {/if} - -
- + +
-
- {#if loading} - + + +
+ {#if loadingHistoricalData}
-

{$_('Loading historical data...')}

-
- {:else if historicalData.length === 0} - -
- {$_('No historical data available for the selected date range.')} + {$_('Loading historical data...')}
- {:else} - -
-

{$_('Stats Summary')}

-
- {#if temperatureChartVisible} - - {/if} - {#if humidityChartVisible} - - {/if} - {#if moistureChartVisible} - - {/if} - {#if co2ChartVisible} - - {/if} - {#if phChartVisible} - - {/if} + {/if} +
+ +
+
+ {#if loading} +
+ +

{$_('Loading historical data...')}

-
- - - -
-

{$_('Data Chart')}

- - -
- {#if renderingVisualization} -
- Rendering chart... -
- {/if} -
-
- {#each numericKeys as key, index (key)} -
-

{$_(key)}

-
-
-
- {/each} -
+ {:else if historicalData.length === 0 && device.cw_device_type?.data_table_v2 !== 'cw_relay_data'} +
+ {$_('No historical data available for the selected date range.')} +
+ {:else if device.cw_device_type?.data_table_v2 === 'cw_relay_data'} + + {:else} +
+

{$_('Stats Summary')}

+
+ {#if temperatureChartVisible}{/if} + {#if humidityChartVisible}{/if} + {#if moistureChartVisible}{/if} + {#if co2ChartVisible}{/if} + {#if phChartVisible}{/if}
-
- {/if} -
- - -
- - -
- -
-
-

{$_('Weather & Data')}

- { - startDateInputString = formatDateForInput(date); - endDateInputString = formatDateForInput(deviceDetail.endDate); - handleDateRangeSubmit(); - }} - /> + {/if}
+ + +
+ + +
+ +{#if !loading && !loadingHistoricalData && historicalData.length > 0} +
+

{$_('Data Chart')}

+
+ {#if renderingVisualization} +
Rendering chart...
+ {/if} +
+
+ {#each numericKeys as key, index (key)} +
+

{$_(key)}

+
+
+
+ {/each} +
+
+
+
+{/if} + + +
+

{$_('Weather & Data')}

+ +
+ diff --git a/src/routes/legal/EULA/+page.svelte.new b/src/routes/legal/EULA/+page.svelte.new deleted file mode 100644 index c82d752a..00000000 --- a/src/routes/legal/EULA/+page.svelte.new +++ /dev/null @@ -1,408 +0,0 @@ - -
-
-
-

CropWatch Legal

-
-
-
-

サービス利用規約

- -
-
第1章 総則
-

第 1.1 条(本規約について)

-

- このサービス利用規約(以下「本規約」という。)は、合同会社クロップウォッチ(以下「当社」という。) - が提供するデータの提供等に関するクラウドサービス(以下「本サービス」という。)を顧客が利用するにあたり - 遵守すべき事項等を定める。 -

-

第 1.2 条(定義)

-

本規約においては、次の用語はそれぞれ次の意味で使用する。

-
    -
  • - 「管理責任者」とは、顧客を代理して顧客アカウントの管理並びに本サービス利用契約の変更及び解約等を - 行う権限を付与された顧客の役員又は従業員(顧客が個人の場合は当該個人) をいう。 -
  • -
  • - 「顧客」とは、本規約に同意して当社と本サービス利用契約を締結した事業者をいう。 -
  • -
  • - 「顧客アカウント」とは、顧客の管理責任者が作成した本サービスを利用するためのアカウントであり、 - 本サービス利用契約の変更及び解約等を行うことができるアカウントをいう。 -
  • -
  • - 「顧客情報」とは、顧客が本サービスに登録する情報その他第 3.1 - 条に具体的に定める情報を含む、 - 本サービスの利用を通じて顧客から直接又は間接に当社に提供される全ての情報、並びにこれらの情報に - 加工又は分析等を加えた情報(ただし、本データを除く。)をいう。 -
  • -
  • - 「知的財産権」とは、著作権(著作権法第 27 条及び第 28 条に定める権利を含む。)、特許権、 - 実用新案権、意匠権、商標権その他の知的財産権(これらを受ける権利及び出願中の権利を含む。) - をいう。 -
  • -
  • - 「匿名データ」とは、顧客情報及び本データに対して一定の処理(統計的な処理又は解析を含む。) - を加え、顧客及び個人が特定又は識別されないよう加工した情報をいう。 -
  • -
  • - 「パスワード」とは、ログイン ID - と組み合わせて、本サービス上で顧客をその他の顧客と区別するために用いられる - 符号であり、顧客が所定の条件に従って自ら設定する半角英数字及び記号の組み合わせをいう。 -
  • -
  • - 「本サービス利用契約」とは、顧客と当社との間の本サービスの利用に関する契約をいう。 -
  • -
  • - 「本ソフトウェア」とは、本サービスのために当社が提供するソフトウェア(本デバイスに組み込まれるものを含む。) - をいう。 -
  • -
  • - 「本データ」とは、本サービスを通じて収集されるデータ及びこれらに加工又は分析等を加えたデータをいう。 -
  • -
  • - 「本デバイス」とは、本データの収集、送信等のために当社が提供し、又は顧客が準備する情報端末、 - センサ、通信機器その他のデバイスをいう。 -
  • -
  • - 「ログイン - ID」とは、本サービスの利用に際して各顧客をその他の顧客と区別するために用いられる - メールアドレスその他の符号であり、顧客が自ら登録する半角英数字及び記号の組み合わせをいう。 -
  • -
-

第 1.3 条(本規約の適用・変更)

-

- 当社は、顧客に対して、本規約に記載する条件にて本サービス及び本ソフトウェアの利用を許諾する。 - 本規約は、顧客と当社との間の本サービス利用契約に適用される。但し、当社は、本規約とは別に、 - 当社が提供する個別の本サービス(以下「個別サービス」という。)の内容に応じて、当該個別サービスの利用条件 - (名称の如何を問わず、以下「個別条件」といいます。)を定め、当社ウェブサイト上で掲載等する場合があり、 - この場合、個別サービスの利用者は、個別条件に同意のうえ、個別サービスを利用するものとする。
- 前項の場合を含め、顧客及び当社が本サービス利用契約において本規約の条件と異なる条件を合意した場合は、 - 当該本サービス利用契約の条件が本規約の条件に優先するものとする。
- 当社は、いつでも本規約を変更することができるものとする。但し、本規約の変更を行うときは、その - 効力発生時期を定め、かつ、本規約を変更する旨及び変更後の本規約の内容並びにその効力発生時期を - 当社ウェブサイトを通じて公開する方法その他の適切な方法により顧客に周知するものとする。
- 顧客は、前項に基づき定める変更後の本規約の効力発生時期以降に本サービスの利用を継続した場合、 - 変更後の本規約の内容に合意したものとみなされるものとし、かかる合意に基づいて本サービス利用契約の内容も - 変更されるものとする。 -

-

第 1.4 条(本サービス利用契約)

-

- 顧客が当社所定の方法により本サービスの利用を申し込み、当社が当該申込みを承諾することにより - 本サービス利用契約を締結する。但し、当該顧客が過去に当社との間の契約に基づく義務に違反した等 - 合理的な理由がある場合、当社は当該顧客との間の本サービス利用契約の締結を拒絶することができる。 -

-

第 1.5 条(委託)

-

- 当社は、顧客に対する本サービスの提供又は本サービス若しくは本ソフトウェアの開発若しくは保守等に - 関して必要となる業務の全部又は一部を、当社の判断において国内及び国外の第三者(以下「委託先」という。) - に委託することができる。 -

-

第 1.6 条(設備等の準備及び維持)

-

- 顧客は、当社が認めた場合を除き、自己の費用と責任において、当社が定める条件にて本デバイス及び - LoRaWAN 基地局の設置及び設定を行い、本サービス利用契約の期間中、本サービス(本デバイス及び - LoRaWAN 基地局 を含む。)が利用可能な環境を維持する。
- 顧客は、本サービスを利用するために必要な場合は、自己の責任と費用において、電気通信事業者等の電気通信 - サービスを利用して本デバイスをインターネットに接続する。
顧客は、LoRaWAN - 接続、本デバイス(第 6.1 条第 9 項に基づく保証の期間内のものを除く。)、 - 前項に定めるインターネット接続その他本サービスを利用するための環境に不具合がある場合又はそれらに関する - セキュリティ上の問題が生じた場合、本サービスの利用ができなくなる可能性があることを了承し、自己の費用と - 責任において、十分な対策(セキュリティ対策を含む。)を講じるものとする。
当社は、当該不具合又はセキュリティ上の問題により顧客が本サービスを利用できないことに対して責任を負わない。 -

-
- -
-
第2章 本サービスの利用等
-

第 2.1 条(本サービスの利用)

-

- 本サービス利用契約に基づき、当社は顧客に対して、本ソフトウェアの利用を行うための制限的かつ譲渡不可能な - ライセンスを許諾する。
- 顧客には、本規約及び本サービス利用契約を遵守する限度において、本規約及び本サービス利用契約に定める利用目的 - の範囲内に限り、本サービスを利用する権限が付与されるものとし、本サービスに含まれる全ての権限、著作権その他 - 知的財産権は当社又は当社にライセンスを許諾している者に帰属する。
顧客は、本サービスの利用権を第三者に譲渡することはできず、当社から書面により明示の権限を付与されない限り、 - 本サービスに含まれる如何なる権利も第三者にサブライセンスすることはできない。 -

-

第 2.2 条(アカウントの管理等)

-

- 顧客は、顧客アカウントのログイン ID - 及びパスワードを、自己の責任のもと厳重に管理し、第三者への貸与等本規約に定める - 利用目的に反した一切の利用を行わない。
顧客アカウントのログイン ID - 及びパスワードを用いた本サービスの利用行為は、全て当該顧客による行為とみなすものとし、 - 顧客の同意の有無を問わず、第三者が顧客アカウントのログイン ID - 及びパスワードを用いて本サービスを利用した場合であっても、 - 当該顧客はかかる利用について一切の責任を負う。
また、当該第三者の行為により当社が損害を被った場合、顧客はその損害を補償する。 -

-

第 2.3 条(利用料金)

-

- 本サービスの利用は有料であり、顧客は本サービスの利用の対価として当社所定の利用料金(以下「本サービス利用料金」という。) - を支払わなければならない。
本サービス利用料金の支払方法及び支払時期等は、当社が別途顧客に対して提示する条件に従うものとする。 -

-

第 2.4 条(契約期間)

-

- 本サービス利用契約の契約期間及び更新条件は、当社が別途顧客に対して提示する条件に従うものとする。 -

-
- -
-
第3章 顧客情報の取得・利用等
-

第 3.1 条(顧客情報の取得・利用等)

-

- 顧客は、本サービスの利用に際して、本ソフトウェアを通じて又はその他の方法により、当社が下記各号に定める情報を - 含む顧客情報を取得することに同意する。なお、顧客情報には、個人情報(個人情報の保護に関する法律に定める個人情報をいう。以下同じ。) - が含まれる場合がある。 -

-
    -
  • - 顧客が本サービス利用契約締結時に登録した住所、氏名、メールアドレス等の情報 -
  • -
  • 顧客の管理責任者の氏名、メールアドレス等の情報
  • -
  • - 雇用、委任等の形態を問わず、外形上顧客のために顧客のログイン ID - 及びパスワードを用いて本サービスを利用する個人が利用時に登録した氏名、メールアドレス等の情報 -
  • -
  • 本ソフトウェアの利用履歴
  • -
  • その他、本サービスの性能を実現するために必要な情報
  • -
-

- 顧客は、当社及び委託先が、下記各号に定める目的のために顧客情報を閲覧、利用及び管理することにあらかじめ同意する。 -

-
    -
  • 顧客に対し本サービス及びこれに付随するサービスを提供する目的
  • -
  • メンテナンス又はセキュリティ上の対応等を実施する目的
  • -
  • 顧客へのアフターサービスを実施する目的
  • -
  • 本ソフトウェアを開発、提供又は保守等する目的
  • -
  • - 顧客以外の者に対するアフターサービスその他のサポート体制を向上させる目的 -
  • -
  • 当社の製品・サービスの開発・改善等のために利用する目的
  • -
  • - 統計数値その他匿名データの作成及びその分析のために利用する目的 -
  • -
  • - 当社と顧客との間のコミュニケーション(アンケート、問い合わせ対応等を含む) -
  • -
  • 本サービスの不正利用(なりすまし等)の防止
  • -
  • 紛争、訴訟等の対応
  • -
  • - 当社のその他の事業目的(開発、設計、エンジニアリング、生産、販売又は本サービスの提供・改善を含むがこれらに限られない。)を遂行する目的 -
  • -
-

- 前項に定める場合を除き、当社は、匿名データ以外の顧客情報を第三者に提供するときは、顧客の承諾を得るものとする。 - ただし、以下の場合はこの限りでない。
- ・法令に基づいて、開示が必要であると当社が合理的に判断した場合
- ・人の生命、身体又は財産の保護のために必要がある場合であって、本人の同意を得ることが困難であると判断した場合
- ・公衆衛生の向上又は児童の健全な育成の推進のために特に必要がある場合であって、本人の同意を得ることが困難であると判断した場合
- ・国の機関若しくは地方公共団体又はその委託を受けた者が法令の定める事務を遂行することに対して協力する必要がある場合であって、 - 本人の同意を得ることにより当該事務の遂行に支障を及ぼすおそれがあると判断した場合
・合併その他の事由により本サービスの権利者、サービスの主体が変更され、サービスの継続のため個人情報を移管する必要があると判断した場合 -

-

- 顧客は、個人情報保護法その他の法令を遵守の上、顧客自身の責任において、顧客情報を第三者に提供することができる。 - かかる第三者に対する顧客情報の提供に関して、当社は一切責任を負わず、当該第三者への顧客情報の提供により - 又はこれに関連して当社が損害を被った場合、顧客はその損害を補償する。 -

-

- 顧客は、本サービスの利用による顧客情報の提供に際して、本データの主体又は本デバイスを利用若しくは所持する主体が - 顧客と異なる場合には、情報の取得について当該主体に対して十分な説明を実施するものとする。
顧客は、顧客情報のうち、個人情報に該当するため送信を希望しない情報がある場合には、当該情報の削除を行う又は - 当社に削除を依頼することができる。この場合、当該情報が取得されないことにより、本サービスの機能の一部が利用できなくなることがあることを了承する。 -

-
- -
-
第4章 本データの取得・管理等
-

第 4.1 条(本データの取得・管理等)

-

- 顧客は、本データが、当社が管理するクラウドサーバに送信、保存、管理されることに同意する。
顧客は、当社が、下記各号に定める目的のために本データを閲覧、利用及び管理することをあらかじめ同意する。 -

-
    -
  • 顧客に対し本サービス及びこれに付随するサービスを提供する目的
  • -
  • メンテナンス又はセキュリティ上の対応等を実施する目的
  • -
  • 顧客へのアフターサービスを実施する目的
  • -
  • 本ソフトウェアを開発又は保守等する目的
  • -
  • 統計数値の作成及びその分析のために利用する目的
  • -
  • 本サービスの不正利用(なりすまし等)の防止
  • -
  • 紛争、訴訟等の対応
  • -
-

- 顧客は、当社が匿名データを第三者に提供する場合があることをあらかじめ同意する。
前二項の場合を除き、当社は、匿名データ以外の本データを第三者に提供するときは、顧客の承諾を得るものとする。 - ただし、法令の定めに基づく場合又は権限ある官公署からの要求を受けた場合はこの限りではない。 -

-

- 顧客は、個人情報保護法その他の法令を遵守の上、顧客自身の責任において、本データを第三者に提供することができる。 - かかる第三者に対する本データの提供に関して、当社は一切責任を負わず、当該第三者への本データの提供により又は - これに関連して当社が損害を被った場合、顧客はその損害を補償する。 -

-

- 顧客は、第 2 - 項に基づく当社による本データの利用等及び顧客から第三者への本データの提供に際して、 - 本データの主体又は本デバイスを利用若しくは所持する主体が顧客と異なる場合には、本データの取得について当該主体に対して - 十分な説明を実施するものとする。 -

-

第 4.2 条(利害関係者の同意等)

-

- 顧客は、本データに顧客以外の第三者の情報が含まれる場合は、当該情報を提供する前に、第 4.1 - 条の内容につき 当該第三者の同意を得るものとする。
顧客は、本サービスの利用開始時及びその利用期間中において、前項の同意を取得していること、本データについて - 適法な権利を有していること及び第 4.1 条第 2 項各号に定める当社による本データの利用が第三者の権利を - 侵害しないことを保証する。顧客は、当該保証の違反により第三者との間で生じた紛争等について、顧客の責任に - おいて対処するものとし、当社はこれらに関して一切責任を負わない。 -

-
- -
-
第5章 禁止事項
-

第 5.1 条(禁止事項)

-

- 顧客は、本サービスの利用に際して、下記各号の行為を行ってはならない。なお、当社は、下記各号に該当する行為に - 関連する情報及びデータについては、任意に削除することができる。 -

-
    -
  • - 当社若しくは第三者の知的財産権その他の権利を侵害する行為又は侵害するおそれのある行為 -
  • -
  • - 本サービスの内容や本サービスにおいて利用しうる情報を改ざん又は消去する行為 -
  • -
  • - 匿名データに係る個人又は顧客を識別するために当該匿名データを他の情報と照合し、また、当該本人を特定する行為又はこれらの照合若しくは特定をしようとする行為 -
  • -
  • - 本デバイス又は本ソフトウェアの全部又は一部を変更、リバースエンジニアリング、逆コンパイル、分解し、それらの派生物を生成し、その他の方法でソースコードを解読する等の行為 -
  • -
  • 本規約に定める以外の目的又は方法で、本サービスを利用する行為
  • -
  • - 本規約若しくは本サービス利用契約に反する態様又は当社の判断により不適当とみなした態様で本サービスを利用する行為 -
  • -
  • - 本規約又は本サービス利用契約に違反して、第三者に本サービスを利用させる行為 -
  • -
  • 第三者になりすまして本サービスを利用する行為
  • -
  • 法令又は公序良俗に違反する行為
  • -
  • - 他者を差別若しくは誹謗中傷し、又はその名誉若しくは信用を毀損する行為 -
  • -
  • 詐欺等の犯罪に結びつく又は結びつくおそれがある行為
  • -
  • - わいせつ、児童ポルノ又は児童虐待にあたる画像、文書等を登録する行為 -
  • -
  • 無限連鎖講を開設し、又はこれを勧誘する行為
  • -
  • - コンピュータ・ウイルス等の有害なデータ、コンピュータ・プログラム等を登録する行為 -
  • -
  • - 広告、宣伝若しくは勧誘のために本サービスを利用する行為、又は第三者が嫌悪感を抱く又はそのおそれのある表現を含む情報を本サービスに登録する行為 -
  • -
  • - 本サービスの提供・動作に支障を与える行為、又は与えるおそれのある行為 -
  • -
  • 面識のない者との出会いを目的とした情報を登録する行為
  • -
  • - ある行為が前各号のいずれかに該当することを知りつつ、その行為を助長する態様・目的でリンクを貼る行為 -
  • -
  • その他、当社が不適切と判断する行為
  • -
-
- -
-
第6章 保証・責任
-

第 6.1 条 (保証・責任の制限)

-

- 当社は、本サービスを現状有姿で提供するものとし、本サービス又は本ソフトウェアに瑕疵・バグ等が存在する場合又は - システムの過負荷、不具合等により本サービスの利用等が停止する場合においても、当社は本規約に規定する限度においてのみ - 責任を負うものとする。
- 当社は、当社が必要と判断した場合には、顧客に通知することなくいつでも本サービスの内容を変更し、又は本サービスの - 提供を停止若しくは中止することができるものとする。本サービスの提供を停止又は中止した場合、当社は顧客に対して、月額等で継続的に支払われる利用料の精算を除き、一切責任を負わないものとする。
- 当社は、本サービス及び本ソフトウェアの完全性、有用性、信頼性、真実性、正確性、妥当性、動作保証、特定の目的への適合性、使用機器への適合性、適法性、第三者の権利を侵害していないことその他本サービス及び本ソフトウェアの機能及び性質について一切の保証を行わないものとし、顧客は自らの判断及び責任に基づき本サービスを利用するものとする。
当社は、本サービスを提供する機器又はサーバの故障・トラブル、停電、通信回線の異常及びシステム障害等の不可抗力により発生する障害について、一切責任を負わないものとする。 -

-

第 6.2 条

-

- 顧客の行為が本規約又は本サービス利用契約に違反すると判断した場合、当該顧客への事前の通知なしに、顧客情報及び本データの全部又は一部の削除を行い、本サービスの利用の停止又は制限等、当社が適当と判断する措置を講ずることができる。 - 当社は、前項の規定に基づき当社が講じた措置に起因する損害が発生した場合であっても、一切責任を負わないものとする。 - 前二項の規定は、当社が当該処置を講じることにより当社又は第三者に損害が発生した場合における、顧客の責任を免責するものではない。 -

-

第 6.3 条(顧客情報等による損害)

-

- 当社は、顧客情報又は本データに起因して本サービス又は当社のサーバに支障が生じた場合若しくはそのおそれがある場合、事前に顧客の承諾を得ることなく、自ら又は委託先をして、顧客情報及び本データの一部又は全部の削除等、当社が適当と判断する措置を講ずることができる。 - 当社は、前項の規定に基づき当社が講じた措置に起因する損害が発生した場合であっても、一切責任を負わないものとする。 -

-

第 6.4 条(当社による損害賠償)

-

- 当社は、本サービスに関して顧客に生じた損害について、当社に故意または重過失が認められる場合に限り、その損害を賠償する責任を負うものとし、以下各号の事由により生じた損害については一切責任を負わない。
- ・地震、津波、落雷、洪水等の天変地異、新型コロナウィルス、インフルエンザ、鳥インフルエンザ等の感染症、戦争(日本国外のものを含む)、革命、暴動、テロ行為等の騒乱、本サービスの提供に影響する法改正、輸送機関・通信回線等の事故、その他の不可抗力
- ・当社以外の者が本デバイス、LoRaWAN - 基地局その他本サービスに供される機器又は設備(以下「本件設備等」という。)に対し加えた損傷等
- ・本件設備等以外の設備又は機器に起因する事故
- ・本サービス利用契約締結時点での一般的な技術水準に従った管理をしていたにもかかわらず発生した不具合(第三者によるサーバ攻撃、インターネットのダウン等通信環境の不調、サーバ提供事業者の債務不履行等に起因するものを含む)
- ・本デバイスを通じて収集した情報に誤り・偏り等が含まれることでなされた AI の不適切な学習
- ・顧客以外の第三者が被った損害(本デバイスを装着した個人のバイタルサインが本サービスをもってしても適時・正確に伝達されないことに起因する損害等)
・その他、発生原因を特定できない事故等 -

-

- 当社は、本規約において顧客の責任としている事項および当社が保証しないまたは責任を負わないものとしている事項については、一切の責任を負わない。
第一項の規定にかかわらず、当社の賠償責任は、顧客が直接被った通常の損害に限定されるものとし、付随的損害、間接損害、特別損害、将来の損害および逸失利益にかかる損害については、賠償する責任を負わない。また、賠償すべき損害の金額は、直近12か月間に顧客が当社に支払った本サービス利用料金の合計に相当する金額を上限とする。 -

-
- -
-
第7章 その他
-

第 7.1 条(秘密保持義務)

-

- 顧客及び当社は、本サービスに関して知り得た相手方の技術上又は営業上その他業務上の情報(但し、顧客情報を除く。以下「秘密情報」という。) - を、本サービスの利用の目的の範囲内でのみ使用し、事前に相手方の書面による承諾を得ない限り第三者(当社においては委託先を除く。) - に開示又は漏洩しない。但し、下記各号のいずれかに該当することを立証できる情報は秘密情報に含まれない。
- ・秘密保持義務を負うことなく既に保有している情報
- ・秘密保持義務を負うことなく第三者から正当に入手した情報
- ・相手方から提供を受けた情報によらず、独自に開発した情報
・本規約及び本サービス利用契約に違反することなく、かつ、受領の前後を問わず公知となった情報 -

-

- 前項の定めにかかわらず、顧客及び当社は、秘密情報のうち法令の定めに基づき又は権限ある官公署からの要求により開示すべき情報を、 - 当該法令の定めに基づく開示先又は当該官公署に対し開示することができる。この場合、顧客及び当社は、関連法令に反しない限り、 - 当該開示前に開示する旨を相手方に通知するものとし、開示前に通知を行うことができない場合は開示後速やかにこれを行う。 -

-

第 7.2 条(契約解除)

-

- 顧客は、当社所定の方法によって申し込むことにより、本サービス利用契約を解約することができる。 - この場合の解約に関する条件は、当社が別途顧客に対して提示する条件に従うものとする。
当社は、顧客が本規約又は本サービス利用契約に違反し、一定期間を定めて是正を催告したにもかかわらず顧客がこれを履行しない場合は、 - 書面又は電子メールによる通知を行うことによって本サービス利用契約の全部又は一部を解約することができる。また、 - 当社は、当該違反が是正されるまでの間、顧客による本サービスの利用を一時的に停止することができる。 -

-

第 7.3 条(契約終了後の処理)

-

- 顧客は、本サービス利用契約の終了後は、本サービスを利用することができないものとする。
顧客は、本サービス利用契約の終了後、顧客が顧客情報、本データその他顧客から当社に提供された全てのデータ及び情報を、 - 当社が任意に削除できることに同意する。顧客は、適時に必要な全てのデータ及び情報について自らの責任及び負担で - バックアップを作成するものとし、当社は顧客のデータ及び情報の喪失について一切責任を負わない。 -

-

第 7.4 条(存続条項)

-

- 第 3.1 条乃至第 7.10 条は、本サービス利用契約の終了後も引き続き効力を有する。 -

-

第 7.5 条(地位等の譲渡禁止)

-

- 顧客は、当社の事前の書面による承諾なしに、本規約及び本サービス利用契約上の地位並びに本規約及び本サービス利用契約に基づく - 権利又は義務の全部若しくは一部を第三者に譲渡してはならない。 -

-

第 7.6 条(分離可能性)

-

- 本規約及び本サービス利用契約の各条項は、法律が許す範囲で可能な限り有効となる方法で解釈されるものとし、本規約及び - 本サービス利用契約のいかなる条項についても法律に違反している又は執行不能と判断される場合には、その条項の残りの部分又は - 他の条項を無効又は執行不能にすることなく、その条項はその法律違反の限度においてのみ無効又は執行不能となる。 -

-

第 7.7 条(専属的合意管轄)

-

- 本規約及び本サービス利用契約に基づく顧客及び当社間の紛争に関しては、宮崎地方裁判所を第一審の専属的合意管轄裁判所とする。 -

-

第 7.8 条(準拠法)

-

- 本規約及び本サービス利用契約の成立、効力、履行及び解釈に関する準拠法は、日本法とする。 -

-

第 7.9 条(誠実協議)

-

- 本規約及び本サービス利用契約に定めのない事項並びに定められた事項の解釈について疑義が生じた場合は、両者誠意を持って協議の上解決する。 -

-
-
2024 年 8 月 1 日 制定・施行
-
-
-
- - diff --git a/src/routes/legal/cookie-policy/+page.svelte b/src/routes/legal/cookie-policy/+page.svelte new file mode 100644 index 00000000..9d5d257c --- /dev/null +++ b/src/routes/legal/cookie-policy/+page.svelte @@ -0,0 +1,124 @@ +
+
+
+

+ クッキーポリシー +

+
+
+
+
+

+ 1. + 合同会社クロップウォッチ(以下「当社」といいます。)が提供するサービス(以下「本サービス」といいます。)においては、クッキー(Cookie)その他情報収集モジュール等(以下「クッキー等」といいます。)を使用しています。当社はクッキー等を通じて、本サービスへの利用者のアクセス情報、閲覧情報等を取得することができます。なお、クッキー等を通じて取得するこれらの情報(以下「クッキー情報」といいます。)には、単独で利用者自身を識別し特定できる情報は含まれていません。 +

+
+
+

+ 2. + 利用者は本サービス上でのクッキー等の使用を許可しない場合には、利用者のブラウザの設定等においてクッキー等を無効にしてください。なお、クッキー等を無効にした場合、本サービスの利便性が損なわれたり、本サービスで提供するサービスのご利用範囲が限定されたりすることがあります。 +

+
+
+

+ 3. 本サービスで利用しているクッキー等の内容については、以下をご参照ください。 +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
クッキー名称送信先取得する情報の概要利用目的
Google AnalyticsGoogle LLC +
【例】
+
    +
  • IPアドレス
  • +
  • 端末固有のユニークID
  • +
  • 利用者の行動情報(サイト上のクリックやページ遷移等)
  • +
  • 利用者が閲覧したページのURL
  • +
  • 位置情報
  • +
  • 滞在時間等
  • +
+
+
【例】
+

+ 本サービスの利用状況を把握することにより、サービス向上及び利用者の興味やニーズにより適したサービスを提供するため +

+
sb-dpaoqrcfswnzknixwkll-auth-token.XSupabase Inc利用者のJavaScript Webトークン(JWT)およびリフレッシュトークン認証および認可に使用
+
+ +

+ ※Google Analyticsにおけるデータ取り扱いの詳細はこちらをご確認ください +

+
+ +
+

+ 2024年8月1日 制定・施行 +

+
+
+
+
+ + diff --git a/src/routes/legal/privacy-policy/+page.svelte b/src/routes/legal/privacy-policy/+page.svelte index ad394fcd..dc1e9b78 100644 --- a/src/routes/legal/privacy-policy/+page.svelte +++ b/src/routes/legal/privacy-policy/+page.svelte @@ -1,36 +1,43 @@
-
+
-

プライバシーポリシー

-
+

+ プライバシーポリシー +

+
-
+
-

+

合同会社クロップウォッチ(以下「当社」といいます。)は、当社のサービスを利用する方(以下「利用者」といいます。)の個人情報の取扱いについて、以下のとおりプライバシーポリシー(以下「本ポリシー」といいます。)を定め、個人情報保護の仕組みを構築し、全従業員に個人情報保護の重要性を認識させるとともにその取組みを徹底させることにより、個人情報の保護を推進します。

-

第 1 条 (個人情報)

-

- 「個人情報」とは、個人情報の保護に関する法律( + 第 1 条 (個人情報) + +

+ 「個人情報」とは、個人情報の保護に関する法律(平成十五年法律第五十七号、以下「個人情報保護法」といいます。)にいう「個人情報」を指し、生存する個人に関する情報であって、当該情報に含まれる氏名、生年月日その他の記述等により特定の個人を識別できるもの又は個人識別符号が含まれるものを指します。

-

第 2 条 (個人情報の利用目的)

-

+

+ 第 2 条 (個人情報の利用目的) +

+

当社は、以下の目的に必要な範囲で、利用者の個人情報を取得し、取得した情報を利用させていただきます。以下の目的の範囲を超えて個人情報を利用する場合には、事前に適切な方法で利用者からの同意を得るものとします。

-
    -
  • - (1) 当社のサービス(以下「本サービス」といいます。)を提供するため -
  • -
  • - (2) 本サービスの内容を改良・改善し、又は新サービスを開発するため -
  • +
      +
    • (1) 当社のサービス(以下「本サービス」といいます。)を提供するため
    • +
    • (2) 本サービスの内容を改良・改善し、又は新サービスを開発するため
    • (3) 本サービスの新機能、更新情報、キャンペーン等及び当社が提供する他のサービスのご案内(電子メール、チラシ、その他のダイレクトメールの送付を含みます。)のため @@ -59,17 +66,25 @@
-

第 3 条 (クッキー等の利用)

-

+

+ 第 3 条 (クッキー等の利用) +

+

当社は、本サービスへの利用者のアクセス情報、閲覧情報等を取得するために、クッキー(Cookie)、情報収集モジュール等(以下「クッキー等」といいます。)の技術を使用しています。本サービスで利用しているクッキー等の内容については、当社が別途定めるクッキーポリシーをご確認ください。

-

第 4 条 (個人情報の管理と保護)

-

+

+ 第 4 条 (個人情報の管理と保護) +

+

個人情報の管理は、厳重に行うこととし、次に掲げるときを除き、利用者の同意がない限り、第三者に対し個人情報を開示・提供することはいたしません。また、安全性を考慮し、個人情報への不正アクセス、個人情報の紛失、破壊、改ざん及び漏えい等のリスクに対する予防並びに是正に関する対策を講じます。

-
    +
    • (1) 人の生命、身体又は財産の保護のために必要がある場合であって、利用者の同意を得ることが困難であるとき。 @@ -86,27 +101,37 @@
-

第 5 条 (個人情報の取扱いの委託)

-

+

+ 第 5 条 (個人情報の取扱いの委託) +

+

当社は、利用目的の達成に必要な範囲内において、個人情報の取扱いの全部又は一部を委託する場合がございます。この場合、当社は、委託先としての適格性を十分審査するとともに、契約にあたって守秘義務に関する事項等を定め、委託先に対する必要かつ適切な監督を行います。

-

第 6 条 (個人情報の開示)

-

+

+ 第 6 条 (個人情報の開示) +

+

利用者は、当社に対し、当社の保有する個人情報の開示を請求することができます。当社は、利用者から当該請求を受けたときは、利用者に対し、遅滞なくこれを開示します。ただし、開示することにより次のいずれかに該当する場合は、その全部又は一部を開示しないこともあり、開示しない決定をした場合には、その旨を遅滞なく通知します。

-
    -
  • - (1) 利用者又は第三者の生命、身体、財産その他の権利利益を害するおそれがある場合 -
  • +
      +
    • (1) 利用者又は第三者の生命、身体、財産その他の権利利益を害するおそれがある場合
    • (2) 当社の業務の適正な実施に著しい支障を及ぼすおそれがある場合
    • (3) その他法令に違反することとなる場合
-

第 7 条 (個人情報訂正等)

-

+

+ 第 7 条 (個人情報訂正等) +

+

1. 利用者は、当社の保有する個人情報が誤った情報である場合には、当社に対し、当該個人情報の訂正、追加又は削除(以下「訂正等」といいます。)を請求することができます。
@@ -115,8 +140,12 @@

-

第 8 条 (個人情報の利用停止等)

-

+

+ 第 8 条 (個人情報の利用停止等) +

+

1. 利用者は、当社に対し、当社の保有する個人情報の利用の停止、消去又は第三者提供の停止(以下「利用停止等」といいます。)を請求することができます。
@@ -125,45 +154,76 @@

-

第 9 条 (プライバシーポリシーの変更手続)

-

+

+ 第 9 条 (プライバシーポリシーの変更手続) +

+

当社は本ポリシーの内容を適宜見直し、その改善に努めます。本ポリシーの内容は、法令その他本ポリシーに別段の定めのある事項を除いて、変更することができるものとします。変更後のプライバシーポリシーは、当社所定の方法により、利用者に通知し、又は当社ウェブサイトに掲載したときから効力を生じるものとします。

-

第 10 条 (法令、規範の遵守)

-

+

+ 第 10 条 (法令、規範の遵守) +

+

当社は、保有する個人情報に関して適用される日本の法令、その他規範を遵守します。

-

第 11 条 (苦情及び相談への対応)

-

+

+ 第 11 条 (苦情及び相談への対応) +

+

当社は、個人情報の取扱いに関する利用者からの苦情、相談を受け付け、適切かつ迅速に対応いたします。また、利用者からの当該個人情報の開示、訂正、追加、削除、利用又は提供の拒否等のご要望に対しても、迅速かつ適切に対応いたします。

-

第 12 条 (安全管理措置)

-

+

+ 第 12 条 (安全管理措置) +

+

当社が利用者よりお預かりした個人情報は、個人情報ファイルへのアクセス制限の実施、アクセスログの記録及び外部からの不正アクセス防止のためのセキュリティ対策の実施等、組織的、物理的、人的、技術的施策を講じることで個人情報への不正な侵入、個人情報の紛失、破壊、改ざん、及び漏えい等を防止いたします。万一、利用者の個人情報の漏えい等の事故が発生した場合、当社は、個人情報保護法及び関連するガイドラインに則り、速やかに監督官庁への報告を行うとともに、当該監督官庁の指示に従い、類似事案の発生防止措置及び再発防止措置等の必要な対応を行います。詳細については、別添「個人情報の安全管理措置」をご確認ください。

-

第 13 条 (当社住所・代表者氏名・個人情報保護管理者)

-

+

+ 第 13 条 (当社住所・代表者氏名・個人情報保護管理者) +

+

当社住所、代表者及び個人情報保護管理者の氏名は以下のとおりです。
住所:宮崎県西都市南方806-5
代表者:池水 彩
個人情報保護管理者:池水 彩

-

第 14 条 (お問い合わせ窓口)

-

+

+ 第 14 条 (お問い合わせ窓口) +

+

当社の個人情報の取扱いに関するお問い合せは以下までご連絡ください。
合同会社クロップウォッチ
〒881-0027 宮崎県西都市南方806-5
TEL: 080-4284-3390
- Mail: sayaka@cropwatch.io + Mail: + sayaka@cropwatch.io +

+

+ 2024年8月1日 制定・施行

-

2024年8月1日 制定・施行

diff --git a/static/build-info.json b/static/build-info.json index 3181f0c6..deacef05 100644 --- a/static/build-info.json +++ b/static/build-info.json @@ -1,9 +1,9 @@ { - "commit": "1b2899c", - "branch": "develop", - "author": "CropWatch", - "date": "2025-07-22T05:13:59.056Z", + "commit": "2f4daa7", + "branch": "workingDateTime", + "author": "Kevin Cantrell", + "date": "2025-08-19T17:11:48.136Z", "builder": "kevin@kevin-desktop", "ipAddress": "192.168.1.100", - "timestamp": 1753161239057 + "timestamp": 1755623508137 } diff --git a/supabase/.temp/cli-latest b/supabase/.temp/cli-latest index 5a5e7b9c..322987f9 100644 --- a/supabase/.temp/cli-latest +++ b/supabase/.temp/cli-latest @@ -1 +1 @@ -v2.30.4 \ No newline at end of file +v2.34.3 \ No newline at end of file