From a5005de418126554f89d62a38f16d19dfb2032f4 Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Tue, 5 Aug 2025 11:04:43 +0900 Subject: [PATCH 01/21] style updates, and attemp to fix all red bug --- src/app.css | 19 +++ src/lib/components/DataCard/DataCard.svelte | 2 +- src/lib/components/StatsCard/StatsCard.svelte | 16 +- .../UI/dashboard/DataRowItem.svelte | 23 ++- .../dashboard/DateRangeSelector.svelte | 106 +++++++------ src/lib/services/DeviceDataService.ts | 11 -- src/routes/app/dashboard/+page.svelte | 148 ++++++++---------- .../devices/[devEui]/+page.svelte | 47 +----- static/build-info.json | 8 +- 9 files changed, 170 insertions(+), 210 deletions(-) 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/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 @@

{#if typeof value === 'number'} diff --git a/src/lib/components/StatsCard/StatsCard.svelte b/src/lib/components/StatsCard/StatsCard.svelte index 98439475..6c3e3e07 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 || '') @@ -172,7 +173,12 @@ {#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..824e02af 100644 --- a/src/lib/components/UI/dashboard/DataRowItem.svelte +++ b/src/lib/components/UI/dashboard/DataRowItem.svelte @@ -140,14 +140,15 @@ >{nameToEmoji(primaryDataKey)}
- + {formatNumber({ key: primaryDataKey, value: primaryValue })} {primaryNotation} -
@@ -157,17 +158,15 @@ {nameToEmoji(secondaryDataKey)} -
- - {formatNumber({ key: secondaryDataKey, value: secondaryValue })} - {secondaryNotation} +
+ + {formatNumber({ key: secondaryDataKey, value: secondaryValue })} + + {secondaryNotation} + -
{/if} diff --git a/src/lib/components/dashboard/DateRangeSelector.svelte b/src/lib/components/dashboard/DateRangeSelector.svelte index d18b1cdd..a92a7ec0 100644 --- a/src/lib/components/dashboard/DateRangeSelector.svelte +++ b/src/lib/components/dashboard/DateRangeSelector.svelte @@ -8,50 +8,57 @@ type Props = { startDateInputString: string; endDateInputString: string; - handleDateRangeSubmit: (units?: number) => void; loadingHistoricalData: boolean; error?: string; + onDateChange: () => void; }; let { startDateInputString = $bindable(), endDateInputString = $bindable(), - handleDateRangeSubmit, + onDateChange, loadingHistoricalData, error }: Props = $props(); -
+
-
- -
- - -
-
- - -
-
+ +
+ +
+ +
+ + onDateChange()} + 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" + /> +
+
+ + onDateChange()} + 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/services/DeviceDataService.ts b/src/lib/services/DeviceDataService.ts index ac95a050..55bb26e8 100644 --- a/src/lib/services/DeviceDataService.ts +++ b/src/lib/services/DeviceDataService.ts @@ -187,17 +187,6 @@ export class DeviceDataService implements IDeviceDataService { .order(tableName == 'cw_traffic2' ? 'traffic_hour' : 'created_at', { ascending: false }) // SHIT FIX #3, traffic camera specific .csv(); - // 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[]; - } - // END OF SHIT FIX - return (data || []) as DeviceDataRecord[]; } catch (error) { // Handle errors with a generic response 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..988b3a2a 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 @@ -234,50 +234,15 @@ // Function to handle fetching data for a specific date range async function handleDateRangeSubmit(units?: number) { + debugger; if (!startDateInputString || !endDateInputString) { 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()); - } + let derivedDate = $derived(startDateInputString); + console.log('Fetching data for range:', derivedDate, endDateInputString); 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 @@ -353,7 +318,7 @@
@@ -376,8 +341,8 @@ handleDateRangeSubmit()} />
@@ -448,7 +413,7 @@
- +
diff --git a/static/build-info.json b/static/build-info.json index 3181f0c6..0f5e2bff 100644 --- a/static/build-info.json +++ b/static/build-info.json @@ -1,9 +1,9 @@ { - "commit": "1b2899c", + "commit": "64d2d4a", "branch": "develop", - "author": "CropWatch", - "date": "2025-07-22T05:13:59.056Z", + "author": "Kevin Cantrell", + "date": "2025-08-05T02:03:09.651Z", "builder": "kevin@kevin-desktop", "ipAddress": "192.168.1.100", - "timestamp": 1753161239057 + "timestamp": 1754359389652 } From 0b74487ae5044176883367b7c84c5310c80fa3e8 Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Fri, 8 Aug 2025 01:07:20 +0900 Subject: [PATCH 02/21] working --- .../UI/dashboard/DataRowItem.svelte | 6 +- .../dashboard/DateRangeSelector.svelte | 123 ++++++++++++++---- .../devices/[devEui]/+page.svelte | 114 ++++++++++------ .../devices/[devEui]/device-detail.svelte.ts | 13 +- static/build-info.json | 6 +- 5 files changed, 188 insertions(+), 74 deletions(-) diff --git a/src/lib/components/UI/dashboard/DataRowItem.svelte b/src/lib/components/UI/dashboard/DataRowItem.svelte index 824e02af..0a4bc53c 100644 --- a/src/lib/components/UI/dashboard/DataRowItem.svelte +++ b/src/lib/components/UI/dashboard/DataRowItem.svelte @@ -128,12 +128,12 @@
-
+
{device.name || `Device ${device.dev_eui}`}
-
+
{#if device.latestData}
{#if secondaryDataKey} - +
{nameToEmoji(secondaryDataKey)} void; }; let { - startDateInputString = $bindable(), - endDateInputString = $bindable(), - onDateChange, - loadingHistoricalData, - error + startDateInput = $bindable(), + endDateInput = $bindable(), + loadingHistoricalData = false, + error, + onDateChange }: Props = $props(); + + let startDateInputString = $state(startDateInput.toISOString().split('T')[0]); + let endDateInputString = $state(endDateInput.toISOString().split('T')[0]); + + const handleDateChange = () => { + if (startDateInput && endDateInput) { + // Validate that end date is not in the future (allow today) + debugger; + const today = new Date(); + today.setHours(23, 59, 59, 999); // Set to end of today + + if (endDateInput > today) { + error = 'End date cannot be in the future.'; + // Reset to today + endDateInput = new Date(); + return; + } + + // Validate that start date is not after end date + if (startDateInput > endDateInput) { + error = 'Start date cannot be after end date.'; + // Reset start date to match end date + startDateInput = new Date(endDateInput); + return; + } + + // Clear any previous error + error = undefined; + // Call the parent's callback + onDateChange(); + } else { + error = 'Please select both start and end dates.'; + } + };
-
@@ -39,8 +73,25 @@ id="startDate" type="date" bind:value={startDateInputString} + onchange={() => { + const newStartDate = new Date(startDateInputString); + + // Validate the date is not invalid + if (isNaN(newStartDate.getTime())) { + error = 'Invalid start date format.'; + return; + } + + startDateInput = newStartDate; + + // If start date is after end date, adjust end date + if (startDateInput > endDateInput) { + endDateInput = new Date(startDateInput); + } + + handleDateChange(); + }} max={endDateInputString} - onclick={() => onDateChange()} 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" />
@@ -52,36 +103,62 @@ id="endDate" type="date" bind:value={endDateInputString} + onchange={() => { + const newEndDate = new Date(endDateInputString); + + // Validate the date is not invalid + if (isNaN(newEndDate.getTime())) { + error = 'Invalid end date format.'; + return; + } + + // Check if end date is in the future + const today = new Date(); + today.setHours(23, 59, 59, 999); + + if (newEndDate > today) { + error = 'End date cannot be in the future.'; + // Reset to today + endDateInput = new Date(); + return; + } + + endDateInput = newEndDate; + + // If end date is before start date, adjust start date + if (endDateInput < startDateInput) { + startDateInput = new Date(endDateInput); + } + + handleDateChange(); + }} min={startDateInputString} max={new Date().toISOString().split('T')[0]} - onclick={() => onDateChange()} 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'}

+

{error}

{/if}
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 988b3a2a..81b80261 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 @@ -69,9 +69,24 @@ 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(''); + // Initialize date range - default to past 24 hours + function initializeComponentDateRange(): { start: Date; end: Date } { + // Always default to current date + const endDate: Date = new Date(); // Default to now + const startDate: Date = new Date(endDate.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago + + console.log('Initializing date range with defaults:', { start: startDate, end: endDate }); + return { start: startDate, end: endDate }; + } + + // Initialize dates - force fresh date creation + let startDateInput: Date = $state(new Date(Date.now() - 24 * 60 * 60 * 1000)); // 24 hours ago + let endDateInput: Date = $state(new Date()); // Current date + + console.log('Date inputs initialized:', { + startDateInput: startDateInput.toISOString(), + endDateInput: endDateInput.toISOString() + }); let loadingHistoricalData = $state(false); // Local loading state for historical data fetch let renderingVisualization = $state(false); // Local rendering state for visualization @@ -88,11 +103,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(() => { @@ -166,16 +178,16 @@ }); // 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); - } - }); + // $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 @@ -233,24 +245,43 @@ }; // Function to handle fetching data for a specific date range - async function handleDateRangeSubmit(units?: number) { - debugger; - if (!startDateInputString || !endDateInputString) { + async function handleDateRangeSubmit() { + if (!startDateInput || !endDateInput) { deviceDetail.error = 'Please select both start and end dates.'; return; } - let derivedDate = $derived(startDateInputString); - console.log('Fetching data for range:', derivedDate, endDateInputString); - - const newData = await fetchDataForDateRange(device, finalStartDate, finalEndDate); - if (newData) { - historicalData = newData; // This will trigger $effect for renderVisualization - calendarEvents = updateEvents(newData); // Use newData directly - } else { - calendarEvents = updateEvents(historicalData); + + console.log('Fetching data for range:', startDateInput, endDateInput); + + // Validate dates + if (startDateInput > endDateInput) { + deviceDetail.error = 'Start date cannot be after end date.'; + return; + } + + const today = new Date(); + today.setHours(23, 59, 59, 999); + if (endDateInput > today) { + deviceDetail.error = 'End date cannot be in the future.'; + return; } - loadingHistoricalData = false; + loadingHistoricalData = true; + + try { + const newData = await fetchDataForDateRange(device, startDateInput, endDateInput); + if (newData) { + historicalData = newData; // This will trigger $effect for renderVisualization + 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; + } } // Expose formatDateForDisplay for the template, aliased from helpers @@ -264,7 +295,12 @@
{#if numericKeys.length} - + {/if}
@@ -339,10 +375,10 @@
handleDateRangeSubmit()} + onDateChange={handleDateRangeSubmit} />
@@ -422,8 +458,8 @@ { - startDateInputString = formatDateForInput(date); - endDateInputString = formatDateForInput(deviceDetail.endDate); + startDateInput = date; + endDateInput = new Date(date); handleDateRangeSubmit(); }} /> diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts index ecc35b7f..949ebd2d 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts @@ -3,6 +3,7 @@ import type { DeviceWithType } from '$lib/models/Device'; import type { DeviceDataRecord, DeviceStats } from '$lib/models/DeviceDataRecord'; import { calculateAverage, formatDateForDisplay, hasValue } from '$lib/utilities/helpers'; import { getNumericKeys, getTextColorByKey } from '$lib/utilities/stats'; +import { DateTime } from 'luxon'; import { _ } from 'svelte-i18n'; import { get } from 'svelte/store'; @@ -26,9 +27,9 @@ export function setupDeviceDetail() { let loading = $state(false); let error = $state(null); - // Date range selection - let startDate = $state(new Date()); // Should be Date object - let endDate = $state(new Date()); // Should be Date object + // Date range selection - initialize without cached values + let startDate = $state(new Date(Date.now() - 24 * 60 * 60 * 1000)); // 24 hours ago + let endDate = $state(new Date(Date.now())); // Current time // Libraries and elements let ApexCharts = $state(undefined); @@ -356,10 +357,10 @@ export function setupDeviceDetail() { // Initialize dates function function initializeDateRange() { - const today = new Date(); - const yesterday = new Date(today); - yesterday.setDate(today.getDate() - 1); + const today = new Date(Date.now()); // Force fresh date + const yesterday = DateTime.fromJSDate(today).minus({ days: 1 }).toJSDate(); + console.log('device-detail initializeDateRange called:', { yesterday, today }); startDate = yesterday; // Assign Date object directly endDate = today; // Assign Date object directly } diff --git a/static/build-info.json b/static/build-info.json index 0f5e2bff..2c28663f 100644 --- a/static/build-info.json +++ b/static/build-info.json @@ -1,9 +1,9 @@ { - "commit": "64d2d4a", + "commit": "a5005de", "branch": "develop", "author": "Kevin Cantrell", - "date": "2025-08-05T02:03:09.651Z", + "date": "2025-08-07T14:32:47.895Z", "builder": "kevin@kevin-desktop", "ipAddress": "192.168.1.100", - "timestamp": 1754359389652 + "timestamp": 1754577167896 } From 02db396fd552f2dc6104c52808e1325a4d1a5214 Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Fri, 8 Aug 2025 11:14:00 +0900 Subject: [PATCH 03/21] working --- .../dashboard/DateRangeSelector.svelte | 101 +++++++++--------- .../devices/[devEui]/+page.svelte | 60 ++--------- .../devices/[devEui]/device-detail.svelte.ts | 48 ++------- 3 files changed, 68 insertions(+), 141 deletions(-) diff --git a/src/lib/components/dashboard/DateRangeSelector.svelte b/src/lib/components/dashboard/DateRangeSelector.svelte index e43bb000..95fd57a2 100644 --- a/src/lib/components/dashboard/DateRangeSelector.svelte +++ b/src/lib/components/dashboard/DateRangeSelector.svelte @@ -21,31 +21,38 @@ onDateChange }: Props = $props(); - let startDateInputString = $state(startDateInput.toISOString().split('T')[0]); - let endDateInputString = $state(endDateInput.toISOString().split('T')[0]); + // 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) { - // Validate that end date is not in the future (allow today) - debugger; - const today = new Date(); - today.setHours(23, 59, 59, 999); // Set to end of today - - if (endDateInput > today) { - error = 'End date cannot be in the future.'; - // Reset to today - endDateInput = new Date(); - return; - } - - // Validate that start date is not after end date - if (startDateInput > endDateInput) { - error = 'Start date cannot be after end date.'; - // Reset start date to match end date - startDateInput = new Date(endDateInput); - return; - } - // Clear any previous error error = undefined; // Call the parent's callback @@ -74,19 +81,22 @@ type="date" bind:value={startDateInputString} onchange={() => { - const newStartDate = new Date(startDateInputString); + const parsed = parseInputDateLocal(startDateInputString); - // Validate the date is not invalid - if (isNaN(newStartDate.getTime())) { + if (!parsed) { error = 'Invalid start date format.'; return; } - startDateInput = newStartDate; + // Normalize start to start-of-day + parsed.setHours(0, 0, 0, 0); + startDateInput = parsed; - // If start date is after end date, adjust end date + // If start date is after end date, align end to end-of-day of start if (startDateInput > endDateInput) { - endDateInput = new Date(startDateInput); + const end = new Date(startDateInput); + end.setHours(23, 59, 59, 999); + endDateInput = end; } handleDateChange(); @@ -104,36 +114,29 @@ type="date" bind:value={endDateInputString} onchange={() => { - const newEndDate = new Date(endDateInputString); - - // Validate the date is not invalid - if (isNaN(newEndDate.getTime())) { - error = 'Invalid end date format.'; - return; - } - - // Check if end date is in the future - const today = new Date(); - today.setHours(23, 59, 59, 999); - - if (newEndDate > today) { - error = 'End date cannot be in the future.'; - // Reset to today - endDateInput = new Date(); + const parsed = parseInputDateLocal(endDateInputString); + if (!parsed) { return; } + // Normalize end to end-of-day + parsed.setHours(23, 59, 59, 999); - endDateInput = newEndDate; + // Cap to today end-of-day + const todayEnd = new Date(); + todayEnd.setHours(23, 59, 59, 999); + endDateInput = parsed > todayEnd ? todayEnd : parsed; - // If end date is before start date, adjust start date + // If end before start, align start to start-of-day of end if (endDateInput < startDateInput) { - startDateInput = new Date(endDateInput); + const start = new Date(endDateInput); + start.setHours(0, 0, 0, 0); + startDateInput = start; } handleDateChange(); }} min={startDateInputString} - max={new Date().toISOString().split('T')[0]} + 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" />
@@ -143,7 +146,7 @@ class="h-[45px] w-[25px]" variant="tertiary" iconic - disabled={new Date(endDateInput) >= DateTime.now().minus({ days: 1 }).toJSDate()} + disabled={endDateInput >= new Date(new Date().setHours(23, 59, 59, 999))} onclick={() => { let dayDiff = DateTime.fromJSDate(endDateInput).diff( DateTime.fromJSDate(startDateInput), 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 81b80261..0b1d64ea 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 @@ -69,24 +69,11 @@ let mainChartElements: HTMLElement[] = $state([]); // Holds chart elements for rendering let blushChartElements: HTMLElement[] = $state([]); // Holds brush elements for rendering - // Initialize date range - default to past 24 hours - function initializeComponentDateRange(): { start: Date; end: Date } { - // Always default to current date - const endDate: Date = new Date(); // Default to now - const startDate: Date = new Date(endDate.getTime() - 24 * 60 * 60 * 1000); // 24 hours ago - - console.log('Initializing date range with defaults:', { start: startDate, end: endDate }); - return { start: startDate, end: endDate }; - } - - // Initialize dates - force fresh date creation - let startDateInput: Date = $state(new Date(Date.now() - 24 * 60 * 60 * 1000)); // 24 hours ago - let endDateInput: Date = $state(new Date()); // Current date - - console.log('Date inputs initialized:', { - startDateInput: startDateInput.toISOString(), - endDateInput: endDateInput.toISOString() - }); + // 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 @@ -177,18 +164,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 } = {}; @@ -251,21 +226,6 @@ return; } - console.log('Fetching data for range:', startDateInput, endDateInput); - - // Validate dates - if (startDateInput > endDateInput) { - deviceDetail.error = 'Start date cannot be after end date.'; - return; - } - - const today = new Date(); - today.setHours(23, 59, 59, 999); - if (endDateInput > today) { - deviceDetail.error = 'End date cannot be in the future.'; - return; - } - loadingHistoricalData = true; try { @@ -455,14 +415,8 @@

{$_('Weather & Data')}

- { - startDateInput = date; - endDateInput = new Date(date); - handleDateRangeSubmit(); - }} - /> + +
diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts index 949ebd2d..afb3b4b9 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts @@ -27,9 +27,11 @@ export function setupDeviceDetail() { let loading = $state(false); let error = $state(null); - // Date range selection - initialize without cached values - let startDate = $state(new Date(Date.now() - 24 * 60 * 60 * 1000)); // 24 hours ago - let endDate = $state(new Date(Date.now())); // Current time + // Date range selection - initialize in LOCAL time (today end-of-day and 24h before) + const __endInit = new Date(); + __endInit.setHours(23, 59, 59, 999); + let endDate = $state(__endInit); + let startDate = $state(new Date(__endInit.getTime() - 24 * 60 * 60 * 1000)); // Libraries and elements let ApexCharts = $state(undefined); @@ -157,36 +159,6 @@ export function setupDeviceDetail() { loading = true; error = null; // Clear previous errors - - try { - // Format Date objects to ISO strings for robust API querying - const startQueryParam = start.toISOString(); // Use parameter - const endQueryParam = end.toISOString(); // Use parameter - - const response = await fetch( - `/api/devices/${device.dev_eui}/data?start=${startQueryParam}&end=${endQueryParam}` - ); - - if (!response.ok) { - const errorText = await response.text(); - console.error('Failed to fetch data:', response.status, errorText); - throw new Error(`Failed to fetch data. Server responded with ${response.status}.`); - } - - const newHistoricalData = await response.json(); - if (!Array.isArray(newHistoricalData)) { - console.error('API did not return an array for historical data:', newHistoricalData); - throw new Error('Invalid data format received from server.'); - } - processHistoricalData(newHistoricalData); // Process the newly fetched data - return newHistoricalData; - } catch (err) { - console.error('Error in fetchDataForDateRange:', err); - error = err instanceof Error ? err.message : 'Unknown error occurred while fetching data.'; - return []; // Return empty array on error - } finally { - loading = false; - } } /** @@ -357,12 +329,10 @@ export function setupDeviceDetail() { // Initialize dates function function initializeDateRange() { - const today = new Date(Date.now()); // Force fresh date - const yesterday = DateTime.fromJSDate(today).minus({ days: 1 }).toJSDate(); - - console.log('device-detail initializeDateRange called:', { yesterday, today }); - startDate = yesterday; // Assign Date object directly - endDate = today; // Assign Date object directly + const end = new Date(); + end.setHours(23, 59, 59, 999); + endDate = end; + startDate = new Date(end.getTime() - 24 * 60 * 60 * 1000); } return { From 7a1b7d5b27594748dc22aeb59e7d192aa3414fea Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Fri, 8 Aug 2025 16:30:26 +0900 Subject: [PATCH 04/21] working time selection --- .../dashboard/DateRangeSelector.svelte | 15 ++++++++- .../devices/[devEui]/+page.svelte | 10 ++---- .../devices/[devEui]/device-detail.svelte.ts | 32 ++++++++++++++++++- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/src/lib/components/dashboard/DateRangeSelector.svelte b/src/lib/components/dashboard/DateRangeSelector.svelte index 95fd57a2..73399529 100644 --- a/src/lib/components/dashboard/DateRangeSelector.svelte +++ b/src/lib/components/dashboard/DateRangeSelector.svelte @@ -67,7 +67,20 @@
-
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 0b1d64ea..c34b8fa0 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 @@ -94,12 +94,6 @@ initializeDateRange(); // This sets deviceDetail.startDate and deviceDetail.endDate (Date objects) }); - $effect(() => { - (async () => { - latestData = await data.latestData; - })(); - }); - $effect(() => { if (device.cw_device_type?.data_table_v2 && !channel) { channel = setupRealtimeSubscription( @@ -231,7 +225,9 @@ try { const newData = await fetchDataForDateRange(device, startDateInput, endDateInput); if (newData) { - historicalData = newData; // This will trigger $effect for renderVisualization + 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); diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts index afb3b4b9..3fcf9448 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/device-detail.svelte.ts @@ -144,7 +144,7 @@ export function setupDeviceDetail() { // Parameters 'start' and 'end' are Date objects. // The component's state variables 'startDate' and 'endDate' (if you were accessing them via this context, which you are not directly here) are also Date objects. // This function uses the 'start' and 'end' parameters passed to it. - + debugger; if (!start || !end) { // Validation using parameters error = 'Please select both start and end dates'; // Set component's error state @@ -159,6 +159,36 @@ export function setupDeviceDetail() { loading = true; error = null; // Clear previous errors + + try { + // Format Date objects to ISO strings for robust API querying + const startQueryParam = start.toISOString(); // Use parameter + const endQueryParam = end.toISOString(); // Use parameter + + const response = await fetch( + `/api/devices/${device.dev_eui}/data?start=${startQueryParam}&end=${endQueryParam}` + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Failed to fetch data:', response.status, errorText); + throw new Error(`Failed to fetch data. Server responded with ${response.status}.`); + } + + const newHistoricalData = await response.json(); + if (!Array.isArray(newHistoricalData)) { + console.error('API did not return an array for historical data:', newHistoricalData); + throw new Error('Invalid data format received from server.'); + } + processHistoricalData(newHistoricalData); // Process the newly fetched data + return newHistoricalData; + } catch (err) { + console.error('Error in fetchDataForDateRange:', err); + error = err instanceof Error ? err.message : 'Unknown error occurred while fetching data.'; + return []; // Return empty array on error + } finally { + loading = false; + } } /** From 107dfa880929afe18d0f961cfee1d94890a6d90e Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Fri, 8 Aug 2025 19:01:54 +0900 Subject: [PATCH 05/21] csv export working after update of date time control --- .../components/devices/ExportButton.svelte | 4 +- src/lib/services/DeviceDataService.ts | 73 ++++++++++++++++++- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/src/lib/components/devices/ExportButton.svelte b/src/lib/components/devices/ExportButton.svelte index 8118602e..5dfa5a58 100644 --- a/src/lib/components/devices/ExportButton.svelte +++ b/src/lib/components/devices/ExportButton.svelte @@ -121,7 +121,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 +130,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/services/DeviceDataService.ts b/src/lib/services/DeviceDataService.ts index 55bb26e8..abca9df2 100644 --- a/src/lib/services/DeviceDataService.ts +++ b/src/lib/services/DeviceDataService.ts @@ -184,10 +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 - return (data || []) as DeviceDataRecord[]; + if (error) { + this.errorHandler.logError(error); + throw new Error(`Error fetching CSV data: ${error.message}`); + } + + 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); @@ -416,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'); + } } From 4c9a817a1fd13463366071a76dfe0762a8905bad Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Sat, 9 Aug 2025 17:28:03 +0900 Subject: [PATCH 06/21] styles update and adding refresh button for ios devices --- src/lib/components/Header.svelte | 1 + .../components/SiteWideRefreshButton.svelte | 34 +++ .../components/devices/ExportButton.svelte | 7 +- src/lib/components/global/Breadcrumbs.svelte | 3 + .../api/devices/[devEui]/pdf/+server.ts | 2 +- .../devices/[devEui]/+page.svelte | 268 +++++++++--------- .../devices/[devEui]/Header.svelte | 2 +- 7 files changed, 185 insertions(+), 132 deletions(-) create mode 100644 src/lib/components/SiteWideRefreshButton.svelte diff --git a/src/lib/components/Header.svelte b/src/lib/components/Header.svelte index 3c89877d..edbaa84a 100644 --- a/src/lib/components/Header.svelte +++ b/src/lib/components/Header.svelte @@ -11,6 +11,7 @@ import MaterialIcon from './UI/icons/MaterialIcon.svelte'; import { sidebarStore } from '$lib/stores/SidebarStore.svelte'; import LanguageSelector from './UI/form/LanguageSelector.svelte'; + import SiteWideRefreshButton from './SiteWideRefreshButton.svelte'; let { userName } = $props(); 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/devices/ExportButton.svelte b/src/lib/components/devices/ExportButton.svelte index 5dfa5a58..2d91d661 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'; @@ -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; } 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/routes/api/devices/[devEui]/pdf/+server.ts b/src/routes/api/devices/[devEui]/pdf/+server.ts index 69e7fbc1..64c3ec83 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 } ); } 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 c34b8fa0..c23fdfb6 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 @@ -122,6 +122,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; @@ -274,150 +299,135 @@
-
- -
- -
-

{$_('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} +
+ {$_('No historical data available for the selected date range.')} +
+ {:else} +
+

{$_('Stats Summary')}

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

{$_('Weather & Data')}

- - + {/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/lib/components/UI/dashboard/DataRowItem.svelte b/src/lib/components/UI/dashboard/DataRowItem.svelte index 0a4bc53c..b23e1b67 100644 --- a/src/lib/components/UI/dashboard/DataRowItem.svelte +++ b/src/lib/components/UI/dashboard/DataRowItem.svelte @@ -138,6 +138,8 @@
{nameToEmoji(primaryDataKey)}{nameToEmoji(secondaryDataKey)}
{ - 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/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/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..b032a286 100644 --- a/src/routes/api/devices/[devEui]/downlink/+server.ts +++ b/src/routes/api/devices/[devEui]/downlink/+server.ts @@ -1,86 +1,152 @@ +// 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, + TTI_IS_BASE_URL, + TTI_AS_BASE_URL +} from '$env/static/private'; + +// --- Config --- +// IS (metadata) on EU1, AS (downlink) on AU1 unless you override via env. +const TTI_IS_BASE = TTI_IS_BASE_URL || 'https://cropwatch.eu1.cloud.thethings.industries'; +const TTI_AS_BASE = TTI_AS_BASE_URL || '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/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/line/callback/+server.ts b/src/routes/app/account-settings/general/line/callback/+server.ts index 82fa421b..bb9e1503 100644 --- a/src/routes/app/account-settings/general/line/callback/+server.ts +++ b/src/routes/app/account-settings/general/line/callback/+server.ts @@ -1,4 +1,4 @@ -import { redirect } from '@sveltejs/kit'; +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'; @@ -7,39 +7,46 @@ interface LineTokenResponse { access_token: string; expires_in: number; id_token: string; - refresh_token: string; + refresh_token?: string; scope: string; token_type: string; } - interface LineIdTokenPayload { - iss: string; sub: string; // LINE user ID aud: string; + iss: string; exp: number; iat: number; - amr?: string[]; name?: string; picture?: string; } -export const GET: RequestHandler = async ({ url, locals: { supabase, safeGetSession } }) => { +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 error = url.searchParams.get('error'); - if (error) { - // User canceled or error occurred - return redirect(303, '/app/account-settings/general?line_error=' + encodeURIComponent(error)); - } - if (!code) { - return redirect(303, '/app/account-settings/general?line_error=missing_code'); - } + 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 channelId = PUBLIC_LINE_CHANNEL_ID; - const channelSecret = LINE_CHANNEL_SECRET; // private - if (!channelId || !channelSecret) { - return new Response('LINE env vars missing', { status: 500 }); - } + 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`; @@ -50,41 +57,41 @@ export const GET: RequestHandler = async ({ url, locals: { supabase, safeGetSess grant_type: 'authorization_code', code, redirect_uri: redirectUri, - client_id: channelId, - client_secret: channelSecret + client_id: clientId, + client_secret: clientSecret, + code_verifier: codeVerifier }) }); - if (!tokenRes.ok) { - return redirect(303, '/app/account-settings/general?line_error=token_exchange_failed'); + 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'); - // Verify id_token and extract sub (LINE user ID) - // For brevity, we decode without verifying signature here. - // In production, verify JWT signature: https://developers.line.biz/en/docs/line-login/get-user-profile/ - const idToken = tokenJson.id_token; - const [, payloadB64] = idToken.split('.'); - let lineUserId: string | undefined; - if (payloadB64) { - const jsonStr = Buffer.from( - payloadB64.replace(/-/g, '+').replace(/_/g, '/'), - 'base64' - ).toString('utf-8'); - const payload = JSON.parse(jsonStr) as LineIdTokenPayload; - lineUserId = payload.sub; - } - if (!lineUserId) { + // 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 { session } = await safeGetSession(); - if (!session) redirect(303, '/auth/login'); - await supabase + 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 index 88692398..2e2398ab 100644 --- a/src/routes/app/account-settings/general/line/connect/+server.ts +++ b/src/routes/app/account-settings/general/line/connect/+server.ts @@ -1,28 +1,59 @@ -import { redirect } from '@sveltejs/kit'; +import { redirect, error } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { PUBLIC_LINE_CHANNEL_ID } from '$env/static/public'; -// LINE Login start -// Docs: https://developers.line.biz/en/docs/line-login/integrate-line-login/ -export const GET: RequestHandler = async ({ url, locals: { safeGetSession } }) => { +// 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 channelId = PUBLIC_LINE_CHANNEL_ID; // define in .env as PUBLIC_LINE_CHANNEL_ID - if (!channelId) { - return new Response('LINE channel id missing (PUBLIC_LINE_CHANNEL_ID)', { status: 500 }); - } + 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 state = crypto.randomUUID(); // TODO: persist state to validate in callback 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', channelId); + 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/location/[location_id]/devices/[devEui]/+page.svelte b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/+page.svelte index c23fdfb6..8089e11a 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(); @@ -385,6 +386,9 @@ {#if moistureChartVisible}{/if} {#if co2ChartVisible}{/if} {#if phChartVisible}{/if} + {#if device.cw_device_type?.data_table_v2 === 'cw_relay_data'} + + {/if}
{/if} From 2f4daa73ec10e24d6000d5cf629a670339d50e21 Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Sun, 10 Aug 2025 19:09:29 +0900 Subject: [PATCH 09/21] safety --- src/lib/components/RelayControl.svelte | 155 +++++++++++++++++++++---- 1 file changed, 133 insertions(+), 22 deletions(-) diff --git a/src/lib/components/RelayControl.svelte b/src/lib/components/RelayControl.svelte index 992475a9..c0049d4c 100644 --- a/src/lib/components/RelayControl.svelte +++ b/src/lib/components/RelayControl.svelte @@ -4,19 +4,66 @@ import Spinner from '$lib/components/Spinner.svelte'; import MaterialIcon from '$lib/components/UI/icons/MaterialIcon.svelte'; import type { DeviceWithType } from '$lib/models/Device'; + import { onMount } from 'svelte'; export let device: DeviceWithType; const devEui = device.dev_eui; - // Use regular object + assignments (Svelte will track top-level mutations when reassigned) - let busy: Record = {}; + // Track relay busy state per relay (not per payload) and optimistic on/off state + type RelayKey = 'relay1' | 'relay2'; + let busy: Record = { relay1: false, relay2: false }; + let relayState: Record = { relay1: false, relay2: false }; // false = OFF + let initialLoaded = false; + let loadingInitial = false; - function setBusy(key: string, val: boolean) { - busy = { ...busy, [key]: val }; + function setBusy(relay: RelayKey, val: boolean) { + busy = { ...busy, [relay]: val }; } - async function send(payloadName: keyof typeof DRAGINO_LT22222L_PAYLOADS) { - if (busy[payloadName]) return; - setBusy(payloadName, true); + function coerceBool(v: any): boolean | undefined { + if (v === undefined || v === null) return undefined; + if (typeof v === 'boolean') return v; + if (typeof v === 'number') return v !== 0; + if (typeof v === 'string') { + const s = v.toLowerCase(); + if (['on', 'off'].includes(s)) return s === 'on'; + if (['1', '0'].includes(s)) return s === '1'; + } + return undefined; + } + + async function loadInitialState() { + loadingInitial = true; + try { + const res = await fetch(`/api/devices/${devEui}/status`); + if (res.ok) { + const latest = await res.json(); + // Try several possible field names + const r1 = coerceBool(latest.relay_1 ?? latest.relay1 ?? latest.r1 ?? latest.relayOne); + const r2 = coerceBool(latest.relay_2 ?? latest.relay2 ?? latest.r2 ?? latest.relayTwo); + relayState = { + relay1: r1 ?? relayState.relay1, + relay2: r2 ?? relayState.relay2 + }; + initialLoaded = true; + } else { + // Non-fatal if status not available + initialLoaded = true; + } + } catch (e) { + initialLoaded = true; // proceed with defaults + } finally { + loadingInitial = false; + } + } + + onMount(() => { + void loadInitialState(); + }); + + async function sendCommand(relay: RelayKey, turnOn: boolean) { + if (busy[relay]) return; + setBusy(relay, true); + const payloadName = (relay + (turnOn ? 'On' : 'Off')) as keyof typeof DRAGINO_LT22222L_PAYLOADS; try { const res = await fetch(`/api/devices/${devEui}/downlink`, { method: 'POST', @@ -27,53 +74,117 @@ const txt = await res.text(); showError('Downlink failed: ' + txt); } else { - success('Command sent'); + relayState = { ...relayState, [relay]: turnOn }; + success(`Relay ${relay === 'relay1' ? '1' : '2'} ${turnOn ? 'ON' : 'OFF'}`); } } catch (e) { showError('Downlink failed'); } finally { - setBusy(payloadName, false); + setBusy(relay, false); } } + + function toggleRelay(relay: RelayKey) { + sendCommand(relay, !relayState[relay]); + } + + function bothBusy() { + return busy.relay1 || busy.relay2 || loadingInitial; + } + + async function setBoth(turnOn: boolean) { + if (bothBusy()) return; + // Run in parallel + await Promise.all([sendCommand('relay1', turnOn), sendCommand('relay2', turnOn)]); + // Success toasts handled individually; optionally consolidate here. + }

- Relay Control + Relay Control

+ +
+ + +
+
-

Sends confirmed LoRaWAN downlink via TTI.

+

Sends confirmed LoRaWAN downlink (ON/OFF).

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/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/routes/api/devices/[devEui]/downlink/+server.ts b/src/routes/api/devices/[devEui]/downlink/+server.ts index b032a286..3b64b3c9 100644 --- a/src/routes/api/devices/[devEui]/downlink/+server.ts +++ b/src/routes/api/devices/[devEui]/downlink/+server.ts @@ -7,16 +7,12 @@ import { ErrorHandlingService } from '$lib/errors/ErrorHandlingService'; import { DRAGINO_LT22222L_PAYLOADS } from '$lib/lorawan/dragino'; // SvelteKit v5: use $env/static/private on the server -import { - PRIVATE_TTI_DRAGINO_RELAY_KEY, - TTI_IS_BASE_URL, - TTI_AS_BASE_URL -} from '$env/static/private'; +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 = TTI_IS_BASE_URL || 'https://cropwatch.eu1.cloud.thethings.industries'; -const TTI_AS_BASE = TTI_AS_BASE_URL || 'https://cropwatch.au1.cloud.thethings.industries'; +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 diff --git a/src/routes/api/devices/[devEui]/pdf/+server.ts b/src/routes/api/devices/[devEui]/pdf/+server.ts index 64c3ec83..e2faa4f9 100644 --- a/src/routes/api/devices/[devEui]/pdf/+server.ts +++ b/src/routes/api/devices/[devEui]/pdf/+server.ts @@ -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; 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 8089e11a..5ec34af4 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 @@ -371,10 +371,12 @@

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

- {:else if historicalData.length === 0} + {: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')}

@@ -386,9 +388,6 @@ {#if moistureChartVisible}{/if} {#if co2ChartVisible}{/if} {#if phChartVisible}{/if} - {#if device.cw_device_type?.data_table_v2 === 'cw_relay_data'} - - {/if}
{/if} diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts index ec657fb5..73c9a895 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts @@ -82,6 +82,10 @@ export const load: PageServerLoad = async ({ params, locals, url }) => { schedules = await reportService.getSchedulesByReportId(reportId); } + alertPoints.map((point) => { + point.operator = point.operator === null ? 'null' : point.operator; + }); + return { devEui, locationId: location_id, @@ -184,11 +188,11 @@ export const actions: Actions = { await reportService.createAlertPoint({ report_id: report.report_id, name: point.name, - operator: point.operator, + operator: point.operator == 'null' ? null : point.operator, min: point.min, max: point.max, value: point.value, - hex_color: point.hex_color, + hex_color: point.operator == 'null' ? '#FFFFFF' : point.hex_color, data_point_key: point.data_point_key }); } diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte index 1f7ba4b6..9756ebd1 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte @@ -79,10 +79,6 @@ point.operator === 'range' ? 'range' : point.operator || ('=' as '=' | '>' | '<' | 'range'), - value: - point.operator === '=' || point.operator === '>' || point.operator === '<' - ? point.min || point.max || 0 - : undefined, min: point.min || undefined, max: point.max || undefined, value: point.value || undefined, @@ -111,16 +107,19 @@ schedules.splice( 0, schedules.length, - ...data.schedules.map((schedule: any) => ({ - id: schedule.id?.toString() || crypto.randomUUID(), - frequency: schedule.end_of_week + ...data.schedules.map((schedule: any) => { + let frequency: 'daily' | 'weekly' | 'monthly' = schedule.end_of_week ? 'weekly' : schedule.end_of_month ? 'monthly' - : 'daily', - time: schedule.time || '09:00', - days: schedule.days || [] - })) + : 'daily'; + return { + id: schedule.id?.toString() || crypto.randomUUID(), + frequency, + time: schedule.time || '09:00', + days: schedule.days || [] + }; + }) ); }); } @@ -259,9 +258,11 @@ }) .map((point) => ({ ...point, - value: point.value ? Number(point.value) : undefined, - min: point.min ? Number(point.min) : undefined, - max: point.max ? Number(point.max) : undefined + value: point.value != null ? Number(point.value) : undefined, + min: point.min != null ? Number(point.min) : undefined, + max: point.max != null ? Number(point.max) : undefined, + // Normalize color property for NumberLine component + color: point.hex_color })) ); @@ -373,7 +374,11 @@
- + Alert Point {i + 1} + diff --git a/static/build-info.json b/static/build-info.json index 2c28663f..deacef05 100644 --- a/static/build-info.json +++ b/static/build-info.json @@ -1,9 +1,9 @@ { - "commit": "a5005de", - "branch": "develop", + "commit": "2f4daa7", + "branch": "workingDateTime", "author": "Kevin Cantrell", - "date": "2025-08-07T14:32:47.895Z", + "date": "2025-08-19T17:11:48.136Z", "builder": "kevin@kevin-desktop", "ipAddress": "192.168.1.100", - "timestamp": 1754577167896 + "timestamp": 1755623508137 } From 1f66078c59022057060608c4ec89dba6b130e4aa Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Thu, 21 Aug 2025 11:32:02 +0900 Subject: [PATCH 11/21] now able to add normal display items to chart --- .../settings/reports/create/+page.server.ts | 29 ++++++++++--------- .../settings/reports/create/+page.svelte | 24 +++++++++++++-- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts index 73c9a895..7f332ef7 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.server.ts @@ -5,7 +5,7 @@ import { ReportAlertPointRepository } from '$lib/repositories/ReportAlertPointRe import { ReportRecipientRepository } from '$lib/repositories/ReportRecipientRepository'; import { ReportUserScheduleRepository } from '$lib/repositories/ReportUserScheduleRepository'; import { ErrorHandlingService } from '$lib/errors/ErrorHandlingService'; -import { error, fail, redirect } from '@sveltejs/kit'; +import { error, fail } from '@sveltejs/kit'; import type { ReportAlertPoint, ReportRecipient, ReportUserSchedule } from '$lib/models/Report'; import { DeviceDataService } from '$lib/services/DeviceDataService'; @@ -31,25 +31,28 @@ export const load: PageServerLoad = async ({ params, locals, url }) => { const errorHandler = new ErrorHandlingService(); const deviceRepo = new ReportRepository(locals.supabase, errorHandler); - const dataService = new DeviceDataService(locals.supabase, deviceRepo); + const dataService = new DeviceDataService(locals.supabase, errorHandler); // If reportId is provided, load existing report data let report = null; let alertPoints: ReportAlertPoint[] = []; let recipients: ReportRecipient[] = []; let schedules: ReportUserSchedule[] = []; - let dataKeys = null; + let dataKeys: string[] = []; const latestData = await dataService.getLatestDeviceData(devEui); // pull the latest data for the device keys if (!latestData) { throw error(404, 'No data found for this device'); } - dataKeys = Object.keys(latestData) - .filter((k) => latestData[k] != null) - .reduce((a, k) => ({ ...a, [k]: latestData[k] }), {}); - delete dataKeys['dev_eui']; // remove dev_eui as it's not a data key - delete dataKeys['created_at']; // remove created_at as it's not a data key - delete dataKeys['is_simulated']; // remove updated_at as it's not a data key - dataKeys = Object.keys(dataKeys); // get only the keys + const keysObj = Object.keys(latestData) + .filter((k) => (latestData as any)[k] != null) + .reduce( + (a: Record, k: string) => ({ ...a, [k]: (latestData as any)[k] }), + {} as Record + ); + delete keysObj['dev_eui']; // remove dev_eui as it's not a data key + delete keysObj['created_at']; // remove created_at as it's not a data key + delete keysObj['is_simulated']; // remove updated_at as it's not a data key + dataKeys = Object.keys(keysObj); // get only the keys if (reportId) { // Create service dependencies @@ -219,10 +222,8 @@ export const actions: Actions = { }); } - throw redirect( - 303, - `/app/dashboard/location/${location_id}/devices/${devEui}/settings/reports` - ); + // Return success so the client can show a toast and navigate + return { success: true }; } catch (err) { console.error('Error creating/updating report:', err); diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte index 9756ebd1..97ce18f4 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte @@ -7,6 +7,7 @@ import TextInput from '$lib/components/UI/form/TextInput.svelte'; import MaterialIcon from '$lib/components/UI/icons/MaterialIcon.svelte'; import ExportButton from '$lib/components/devices/ExportButton.svelte'; + import { success as toastSuccess, error as toastError } from '$lib/stores/toast.svelte'; import type { ReportAlertPoint } from '$lib/models/Report.js'; import type { ActionResult } from '@sveltejs/kit'; import { untrack } from 'svelte'; @@ -79,6 +80,7 @@ point.operator === 'range' ? 'range' : point.operator || ('=' as '=' | '>' | '<' | 'range'), + data_point_key: point.data_point_key || '', min: point.min || undefined, max: point.max || undefined, value: point.value || undefined, @@ -271,8 +273,24 @@ isSubmitting = true; return async ({ result }: { result: ActionResult }) => { isSubmitting = false; - if (result.type === 'success') { - goto(`/app/dashboard/location/${locationId}/devices/${devEui}/settings/reports`); + switch (result.type) { + case 'success': { + toastSuccess(isEditing ? 'Report updated' : 'Report created'); + goto(`/app/dashboard/location/${locationId}/devices/${devEui}/settings/reports`); + break; + } + case 'failure': { + const msg = ((result as any).data?.message ?? + (result as any).data?.error ?? + 'Failed to save report') as string; + toastError(msg); + break; + } + default: { + // Covers 'error' and any unexpected type + const msg = ((result as any)?.error?.message ?? 'Failed to save report') as string; + toastError(msg); + } } }; } @@ -373,6 +391,7 @@ {#each alertPoints as point, i}
+
{JSON.stringify(point, null, 2)}
{ point.operator === null; + point.value = undefined; }} class="w-full" > From b81b2017702791489c501a9963d33df34c2bdbb6 Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Sat, 23 Aug 2025 02:17:05 +0900 Subject: [PATCH 12/21] permissions updates going very well! --- database.types.ts | 312 +++----------- src/hooks.server.ts | 1 + src/lib/components/StatsCard/StatsCard.svelte | 6 +- .../UI/form/UserPermissionsSelector.svelte | 5 +- .../components/devices/ExportButton.svelte | 2 +- src/lib/repositories/DeviceRepository.ts | 1 + src/lib/repositories/LocationRepository.ts | 16 + src/lib/services/DeviceService.ts | 124 +++--- .../devices/[devEui]/+page.svelte | 18 +- .../settings/permissions/+page.svelte | 8 +- .../settings/reports/create/+page.svelte | 1 - src/routes/auth/register/+page.svelte | 42 +- src/routes/legal/EULA/+page.svelte.new | 408 ------------------ src/routes/legal/cookie-policy/+page.svelte | 124 ++++++ src/routes/legal/privacy-policy/+page.svelte | 156 ++++--- supabase/.temp/cli-latest | 2 +- 16 files changed, 429 insertions(+), 797 deletions(-) delete mode 100644 src/routes/legal/EULA/+page.svelte.new create mode 100644 src/routes/legal/cookie-policy/+page.svelte diff --git a/database.types.ts b/database.types.ts index 81fb6103..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: { @@ -386,54 +386,6 @@ export type Database = { }; Relationships: []; }; - cw_air_thvd: { - Row: { - created_at: string; - dev_eui: string; - dewPointC: number | null; - humidity: number; - id: number; - profile_id: string | null; - temperatureC: number; - vpd: number | null; - }; - Insert: { - created_at?: string; - dev_eui: string; - dewPointC?: number | null; - humidity: number; - id?: number; - profile_id?: string | null; - temperatureC: number; - vpd?: number | null; - }; - Update: { - created_at?: string; - dev_eui?: string; - dewPointC?: number | null; - humidity?: number; - id?: number; - profile_id?: string | null; - temperatureC?: number; - vpd?: number | null; - }; - Relationships: [ - { - foreignKeyName: 'cw_air_thvd_dev_eui_fkey'; - columns: ['dev_eui']; - isOneToOne: false; - referencedRelation: 'cw_devices'; - referencedColumns: ['dev_eui']; - }, - { - foreignKeyName: 'public_cw_air_thvd_profile_id_fkey'; - columns: ['profile_id']; - isOneToOne: false; - referencedRelation: 'profiles'; - referencedColumns: ['id']; - } - ]; - }; cw_data_metadata: { Row: { adder: number; @@ -757,6 +709,7 @@ export type Database = { }; cw_location_owners: { Row: { + admin_user_id: string; description: string | null; id: number; is_active: boolean | null; @@ -766,6 +719,7 @@ export type Database = { user_id: string; }; Insert: { + admin_user_id: string; description?: string | null; id?: number; is_active?: boolean | null; @@ -775,6 +729,7 @@ export type Database = { user_id: string; }; Update: { + admin_user_id?: string; description?: string | null; id?: number; is_active?: boolean | null; @@ -921,6 +876,41 @@ export type Database = { } ]; }; + cw_relay_data: { + Row: { + created_at: string; + dev_eui: string; + id: number; + last_update: string; + relay_1: boolean | null; + relay_2: boolean | null; + }; + Insert: { + created_at?: string; + dev_eui: string; + id?: number; + last_update: string; + relay_1?: boolean | null; + relay_2?: boolean | null; + }; + Update: { + created_at?: string; + dev_eui?: string; + id?: number; + last_update?: string; + relay_1?: boolean | null; + relay_2?: boolean | null; + }; + Relationships: [ + { + foreignKeyName: 'cw_relay_data_dev_eui_fkey'; + columns: ['dev_eui']; + isOneToOne: true; + referencedRelation: 'cw_devices'; + referencedColumns: ['dev_eui']; + } + ]; + }; cw_rule_criteria: { Row: { created_at: string; @@ -977,6 +967,7 @@ export type Database = { notifier_type: number; profile_id: string; ruleGroupId: string; + send_using: string | null; trigger_count: number; }; Insert: { @@ -990,6 +981,7 @@ export type Database = { notifier_type: number; profile_id?: string; ruleGroupId: string; + send_using?: string | null; trigger_count?: number; }; Update: { @@ -1003,6 +995,7 @@ export type Database = { notifier_type?: number; profile_id?: string; ruleGroupId?: string; + send_using?: string | null; trigger_count?: number; }; Relationships: [ @@ -1056,36 +1049,6 @@ export type Database = { }; Relationships: []; }; - 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; @@ -1414,7 +1377,7 @@ export type Database = { name: string; operator: string | null; report_id: string; - user_id: string | null; + user_id: string; value: number | null; }; Insert: { @@ -1427,7 +1390,7 @@ export type Database = { name: string; operator?: string | null; report_id: string; - user_id?: string | null; + user_id?: string; value?: number | null; }; Update: { @@ -1440,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: [ @@ -1643,92 +1606,6 @@ export type Database = { } ]; }; - seeed_sensecap_s2104: { - Row: { - battery_level: number | null; - created_at: string; - dev_eui: string; - id: number; - moisture: number | null; - temperature: number | null; - }; - Insert: { - battery_level?: number | null; - created_at?: string; - dev_eui: string; - id?: number; - moisture?: number | null; - temperature?: number | null; - }; - Update: { - battery_level?: number | null; - created_at?: string; - dev_eui?: string; - id?: number; - moisture?: number | null; - temperature?: number | null; - }; - Relationships: [ - { - foreignKeyName: 'seeed_sensecap_s210x_temp_moist_dev_eui_fkey'; - columns: ['dev_eui']; - isOneToOne: false; - referencedRelation: 'cw_devices'; - referencedColumns: ['dev_eui']; - } - ]; - }; - seeed_t1000: { - Row: { - battery_level: number | null; - created_at: string; - dev_eui: string; - id: number; - lat: number; - long: number; - profile_id: string | null; - sos: number | null; - temperatureC: number | null; - }; - Insert: { - battery_level?: number | null; - created_at?: string; - dev_eui: string; - id?: number; - lat: number; - long: number; - profile_id?: string | null; - sos?: number | null; - temperatureC?: number | null; - }; - Update: { - battery_level?: number | null; - created_at?: string; - dev_eui?: string; - id?: number; - lat?: number; - long?: number; - profile_id?: string | null; - sos?: number | null; - temperatureC?: number | null; - }; - Relationships: [ - { - foreignKeyName: 'SEEED_T1000_dev_eui_fkey'; - columns: ['dev_eui']; - isOneToOne: false; - referencedRelation: 'cw_devices'; - referencedColumns: ['dev_eui']; - }, - { - foreignKeyName: 'seeed_t1000_profile_id_fkey'; - columns: ['profile_id']; - isOneToOne: false; - referencedRelation: 'profiles'; - referencedColumns: ['id']; - } - ]; - }; stripe_customers: { Row: { attrs: Json | null; @@ -1816,63 +1693,6 @@ export type Database = { }; Relationships: []; }; - temp_humid_co2_alert_settings: { - Row: { - action: number; - cleared: boolean; - created_at: string; - dev_eui: string; - id: number; - OneSignalID: string | null; - operator: string; - profile_id: string; - receiver: string; - subject: string; - value: number; - }; - Insert: { - action: number; - cleared: boolean; - created_at?: string; - dev_eui: string; - id?: number; - OneSignalID?: string | null; - operator: string; - profile_id: string; - receiver: string; - subject: string; - value: number; - }; - Update: { - action?: number; - cleared?: boolean; - created_at?: string; - dev_eui?: string; - id?: number; - OneSignalID?: string | null; - operator?: string; - profile_id?: string; - receiver?: string; - subject?: string; - value?: number; - }; - Relationships: [ - { - foreignKeyName: 'temp_humid_co2_alert_settings_dev_eui_fkey'; - columns: ['dev_eui']; - isOneToOne: false; - referencedRelation: 'devices'; - referencedColumns: ['dev_eui']; - }, - { - foreignKeyName: 'temp_humid_co2_alert_settings_profile_id_fkey'; - columns: ['profile_id']; - isOneToOne: false; - referencedRelation: 'profiles'; - referencedColumns: ['id']; - } - ]; - }; user_discord_connections: { Row: { access_token: string; @@ -1924,46 +1744,46 @@ export type Database = { }; 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: { @@ -1973,8 +1793,8 @@ export type Database = { get_road_events: { Args: { time_grouping: string }; Returns: { - group_period: string; event_count: number; + group_period: string; }[]; }; get_road_events_summary1: { @@ -1986,8 +1806,8 @@ export type Database = { time_span: string; }; Returns: { - period_start: string; count: number; + period_start: string; }[]; }; }; 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/StatsCard/StatsCard.svelte b/src/lib/components/StatsCard/StatsCard.svelte index 6c3e3e07..6e5a1bb6 100644 --- a/src/lib/components/StatsCard/StatsCard.svelte +++ b/src/lib/components/StatsCard/StatsCard.svelte @@ -172,8 +172,10 @@ {/if} {#if expandable} -
-
+
+
{: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/devices/ExportButton.svelte b/src/lib/components/devices/ExportButton.svelte index 2d91d661..41c12066 100644 --- a/src/lib/components/devices/ExportButton.svelte +++ b/src/lib/components/devices/ExportButton.svelte @@ -25,7 +25,7 @@ buttonLabel, disabled = false, showDatePicker = true, - types = ['csv', 'pdf'], + types = ['csv'], // 'pdf'], startDateInputString = undefined, endDateInputString = undefined, alertPoints = [], 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/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/routes/app/dashboard/location/[location_id]/devices/[devEui]/+page.svelte b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/+page.svelte index 5ec34af4..0f111417 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 @@ -36,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 { @@ -274,6 +279,8 @@ Device Details - {device?.name || device?.dev_eui} +{devicePermissionLevel} +
{#if numericKeys.length} @@ -284,10 +291,13 @@ dataKeys={numericKeys} /> {/if} - + + {#if device.user_id == userId || devicePermissionLevel === 2 || devicePermissionLevel === 1} + + {/if}
- + diff --git a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte index 97ce18f4..75ce396d 100644 --- a/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte +++ b/src/routes/app/dashboard/location/[location_id]/devices/[devEui]/settings/reports/create/+page.svelte @@ -391,7 +391,6 @@ {#each alertPoints as point, i}
-
{JSON.stringify(point, null, 2)}
@@ -480,7 +482,7 @@ name="privacy" type="checkbox" bind:checked={agreedToPrivacy} - on:change={validateTermsAgreement} + onchange={validateTermsAgreement} class="focus:ring-primary-light dark:focus:ring-primary-dark text-primary-light dark:text-primary-dark h-4 w-4 rounded border-gray-300 dark:border-gray-700" disabled={isSubmitting} required @@ -489,7 +491,9 @@
@@ -503,7 +507,7 @@ name="cookies" type="checkbox" bind:checked={agreedToCookies} - on:change={validateTermsAgreement} + onchange={validateTermsAgreement} class="focus:ring-primary-light dark:focus:ring-primary-dark text-primary-light dark:text-primary-dark h-4 w-4 rounded border-gray-300 dark:border-gray-700" disabled={isSubmitting} required @@ -512,7 +516,9 @@
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/supabase/.temp/cli-latest b/supabase/.temp/cli-latest index 8e00c6d6..322987f9 100644 --- a/supabase/.temp/cli-latest +++ b/supabase/.temp/cli-latest @@ -1 +1 @@ -v2.33.9 \ No newline at end of file +v2.34.3 \ No newline at end of file From f433fddb517ffb26df39fbd3807d91f1125f3d91 Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Sat, 23 Aug 2025 02:20:04 +0900 Subject: [PATCH 13/21] better permissions around download and settings --- .../location/[location_id]/devices/[devEui]/+page.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0f111417..b91189f4 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 @@ -283,7 +283,7 @@
- {#if numericKeys.length} + {#if (numericKeys.length && device.user_id == userId) || devicePermissionLevel <= 2} {/if} - {#if device.user_id == userId || devicePermissionLevel === 2 || devicePermissionLevel === 1} + {#if device.user_id == userId || devicePermissionLevel === 1} +
From d76c451fd2e76d50101fddae4a73a51adb4351df Mon Sep 17 00:00:00 2001 From: Kevin Cantrell Date: Sat, 23 Aug 2025 18:32:38 +0900 Subject: [PATCH 21/21] EULA UPDATED" --- src/routes/legal/EULA/+page.svelte | 1109 ++++++++++++++++++---------- 1 file changed, 730 insertions(+), 379 deletions(-) diff --git a/src/routes/legal/EULA/+page.svelte b/src/routes/legal/EULA/+page.svelte index c82d752a..d00d07f7 100644 --- a/src/routes/legal/EULA/+page.svelte +++ b/src/routes/legal/EULA/+page.svelte @@ -1,408 +1,759 @@ -
-
+
-

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 項に基づく保蚌の期間内のものを陀く。)、 - 前項に定めるむンタヌネット接続その他本サヌビスを利甚するための環境に䞍具合がある堎合又はそれらに関する - セキュリティ䞊の問題が生じた堎合、本サヌビスの利甚ができなくなる可胜性があるこずを了承し、自己の費甚ず - 責任においお、十分な察策(セキュリティ察策を含む。)を講じるものずする。
圓瀟は、圓該䞍具合又はセキュリティ䞊の問題により顧客が本サヌビスを利甚できないこずに察しお責任を負わない。 -

+

+ 第1章 総則 +

+
+
+

+ 第1.1条本芏玄に぀いお +

+

+ このサヌビス利甚芏玄以䞋「本芏玄」ずいう。は、合同䌚瀟クロップりォッチ以䞋「圓瀟」ずいう。が提䟛するデヌタの提䟛等に関するクラりドサヌビス以䞋「本サヌビス」ずいう。を利甚者が利甚するにあたり遵守すべき事項等を定める。 +

+
+
+

+ 第1.2条定矩 +

+

+ 本芏玄においおは、次の甚語はそれぞれ次の意味で䜿甚する。 +

+
    +
  • + (1) + 「管理責任者」ずは、顧客を代理しお顧客の利甚者アカりントの管理䞊びに本サヌビス利甚契玄の倉曎及び解玄等を行う暩限を付䞎された顧客の圹員又は埓業員顧客が個人の堎合は圓該個人をいう。 +
  • +
  • + (2) 「顧客」ずは、本芏玄に同意しお圓瀟ず本サヌビス利甚契玄を締結した事業者をいう。 +
  • +
  • + (3) + 「知的財産暩」ずは、著䜜暩著䜜暩法第27条及び第28条に定める暩利を含む。、特蚱暩、実甚新案暩、意匠暩、商暙暩その他の知的財産暩これらを受ける暩利及び出願䞭の暩利を含む。をいう。 +
  • +
  • + (4) + 「匿名デヌタ」ずは、利甚者情報及び本デヌタに察しお䞀定の凊理統蚈的な凊理又は解析を含む。を加え、利甚者を含む個人が特定又は識別されないよう加工した情報をいう。 +
  • +
  • + (5) + 「パスワヌド」ずは、ログむンIDず組み合わせお、本サヌビス䞊で利甚者をその他の利甚者ず区別するために甚いられる笊号であり、利甚者が所定の条件に埓っお自ら蚭定する半角英数字及び蚘号の組み合わせをいう。 +
  • +
  • + (6) + 「本サヌビス利甚契玄」ずは、顧客ず圓瀟ずの間の本サヌビスの利甚に関する契玄をいう。 +
  • +
  • + (7) + 「本゜フトりェア」ずは、本サヌビスのために圓瀟が提䟛する゜フトりェア本ハヌドりェアに組み蟌たれるものを含む。をいう。 +
  • +
  • + (8) + 「本デヌタ」ずは、本サヌビスを通じお収集されるデヌタ及びこれらに加工又は分析等を加えたデヌタをいう。 +
  • +
  • + (9) + 「本ハヌドりェア」ずは、本デヌタの収集、送信、機噚の自動化等のために圓瀟が提䟛し、又は利甚者が準備する情報端末、センサ、通信機噚その他のハヌドりェアをいう。 +
  • +
  • + (10) + 「利甚者」ずは、本芏玄に同意しお本サヌビスを利甚する顧客の圹職員その他の顧客が指定した個人をいう。 +
  • +
  • + (11) + 「利甚者アカりント」ずは、顧客及び利甚者が䜜成した本サヌビスを利甚するためのアカりントをいう。 +
  • +
  • + (12) + 「利甚者情報」ずは、顧客又は利甚者が本サヌビスに登録する情報その他第3.1条に具䜓的に定める情報を含む、本サヌビスの利甚を通じお顧客又は利甚者から盎接又は間接に圓瀟に提䟛される党おの情報、䞊びにこれらの情報に加工又は分析等を加えた情報ただし、本デヌタを陀く。をいう。 +
  • +
  • + (13) + 「ログむンID」ずは、本サヌビスの利甚に際しお各利甚者をその他の利甚者ず区別するために甚いられるメヌルアドレスその他の笊号であり、利甚者が自ら登録する半角英数字及び蚘号の組み合わせをいう。 +
  • +
+
+
+

+ 第1.3条本芏玄の適甚・倉曎 +

+
    +
  1. + 圓瀟は、顧客及び利甚者に察しお、本芏玄に蚘茉する条件にお本サヌビス及び本゜フトりェアの利甚を蚱諟する。 +
  2. +
  3. + 本芏玄は、顧客ず圓瀟ずの間の本サヌビス利甚契玄及び利甚者による本サヌビスの利甚に適甚される。䜆し、圓瀟は、本芏玄ずは別に、圓瀟が提䟛する個別の本サヌビス以䞋「個別サヌビス」ずいう。の内容に応じお、圓該個別サヌビスの利甚条件名称の劂䜕を問わず、以䞋「個別条件」ずいいたす。を定め、圓瀟りェブサむト䞊で掲茉等する堎合があり、この堎合、個別サヌビスの利甚者は、個別条件に同意のうえ、個別サヌビスを利甚するものずする。 +
  4. +
  5. + 前項の堎合を含め、顧客及び圓瀟が本サヌビス利甚契玄においお本芏玄の条件ず異なる条件を合意した堎合は、圓該本サヌビス利甚契玄の条件が本芏玄の条件に優先するものずする。 +
  6. +
  7. + 圓瀟は、い぀でも本芏玄を倉曎するこずができるものずする。䜆し、本芏玄の倉曎を行うずきは、その効力発生時期を定め、か぀、本芏玄を倉曎する旚及び倉曎埌の本芏玄の内容䞊びにその効力発生時期を圓瀟りェブサむトを通じお公開する方法その他の適切な方法により顧客及び利甚者に呚知するものずする。 +
  8. +
  9. + 顧客及び利甚者は、前項に基づき定める倉曎埌の本芏玄の効力発生時期以降に本サヌビスの利甚を継続した堎合、倉曎埌の本芏玄の内容に合意したものずみなされるものずし、かかる合意に基づいお本サヌビス利甚契玄の内容も倉曎されるものずする。 +
  10. +
+
+
+

+ 第1.4条本サヌビス利甚契玄 +

+

+ 顧客が圓瀟所定の方法により本サヌビスの利甚を申し蟌み、圓瀟が圓該申蟌みを承諟するこずにより本サヌビス利甚契玄を締結する。䜆し、圓該顧客が過去に圓瀟ずの間の契玄に基づく矩務に違反した等合理的な理由がある堎合、圓瀟は圓該顧客ずの間の本サヌビス利甚契玄の締結を拒絶するこずができる。 +

+
+
+

+ 第1.5条委蚗 +

+

+ 圓瀟は、顧客及び利甚者に察する本サヌビスの提䟛又は本サヌビス若しくは本゜フトりェアの開発若しくは保守等に関しお必芁ずなる業務の党郚又は䞀郚を、圓瀟の刀断においお囜内及び囜倖の第䞉者以䞋「委蚗先」ずいう。に委蚗するこずができる。 +

+
+
+

+ 第1.6条蚭備等の準備及び維持 +

+
    +
  1. + 顧客は、圓瀟が認めた堎合を陀き、自己の費甚ず責任においお、圓瀟が定める条件にお本ハヌドりェア及びLoRaWAN基地局の蚭眮及び蚭定を行い、本サヌビス利甚契玄の期間䞭、本サヌビス本ハヌドりェア及びLoRaWAN基地局を含む。が利甚可胜な環境を維持する。 +
  2. +
  3. + 顧客は、本サヌビスを利甚するために必芁な堎合は、自己の責任ず費甚においお、電気通信事業者等の電気通信サヌビスを利甚しおLoRaWAN基地局をむンタヌネットに接続する。 +
  4. +
  5. + 顧客は、LoRaWAN接続、本ハヌドりェア第6.1条第9項に基づく保蚌の期間内のものを陀く。、前項に定めるむンタヌネット接続その他本サヌビスを利甚するための環境に䞍具合がある堎合又はそれらに関するセキュリティ䞊の問題が生じた堎合、本サヌビスの利甚ができなくなる可胜性があるこずを了承し、自己の費甚ず責任においお、十分な察策セキュリティ察策を含む。を講じるものずする。圓瀟は、圓該䞍具合又はセキュリティ䞊の問題により顧客又は利甚者が本サヌビスを利甚できないこずに察しお責任を負わない。 +
  6. +
+
+
- +
-
第2ç«  本サヌビスの利甚等
-

第 2.1 条(本サヌビスの利甚)

-

- 本サヌビス利甚契玄に基づき、圓瀟は顧客に察しお、本゜フトりェアの利甚を行うための制限的か぀譲枡䞍可胜な - ラむセンスを蚱諟する。
- 顧客には、本芏玄及び本サヌビス利甚契玄を遵守する限床においお、本芏玄及び本サヌビス利甚契玄に定める利甚目的 - の範囲内に限り、本サヌビスを利甚する暩限が付䞎されるものずし、本サヌビスに含たれる党おの暩限、著䜜暩その他 - 知的財産暩は圓瀟又は圓瀟にラむセンスを蚱諟しおいる者に垰属する。
顧客は、本サヌビスの利甚暩を第䞉者に譲枡するこずはできず、圓瀟から曞面により明瀺の暩限を付䞎されない限り、 - 本サヌビスに含たれる劂䜕なる暩利も第䞉者にサブラむセンスするこずはできない。 -

-

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

-

- 顧客は、顧客アカりントのログむン ID - 及びパスワヌドを、自己の責任のもず厳重に管理し、第䞉者ぞの貞䞎等本芏玄に定める - 利甚目的に反した䞀切の利甚を行わない。
顧客アカりントのログむン ID - 及びパスワヌドを甚いた本サヌビスの利甚行為は、党お圓該顧客による行為ずみなすものずし、 - 顧客の同意の有無を問わず、第䞉者が顧客アカりントのログむン ID - 及びパスワヌドを甚いお本サヌビスを利甚した堎合であっおも、 - 圓該顧客はかかる利甚に぀いお䞀切の責任を負う。
たた、圓該第䞉者の行為により圓瀟が損害を被った堎合、顧客はその損害を補償する。 -

-

第 2.3 条(利甚料金)

-

- 本サヌビスの利甚は有料であり、顧客は本サヌビスの利甚の察䟡ずしお圓瀟所定の利甚料金(以䞋「本サヌビス利甚料金」ずいう。) - を支払わなければならない。
本サヌビス利甚料金の支払方法及び支払時期等は、圓瀟が別途顧客に察しお提瀺する条件に埓うものずする。 -

-

第 2.4 条(契玄期間)

-

- 本サヌビス利甚契玄の契玄期間及び曎新条件は、圓瀟が別途顧客に察しお提瀺する条件に埓うものずする。 -

+

+ 第2章 本サヌビスの利甚等 +

+
+
+

+ 第2.1条本サヌビスの利甚 +

+
    +
  1. + 本サヌビス利甚契玄に基づき、圓瀟は顧客及び利甚者に察しお、本゜フトりェアの利甚を行うための制限的か぀譲枡䞍可胜なラむセンスを蚱諟する。 +
  2. +
  3. + 顧客及び利甚者には、本芏玄及び本サヌビス利甚契玄を遵守する限床においお、本芏玄及び本サヌビス利甚契玄に定める利甚目的の範囲内に限り、本サヌビスを利甚する暩限が付䞎されるものずし、本サヌビスに含たれる党おの暩限、著䜜暩その他知的財産暩は圓瀟又は圓瀟にラむセンスを蚱諟しおいる者に垰属する。 +
  4. +
  5. + 顧客及び利甚者は、本サヌビスの利甚暩を第䞉者に譲枡するこずはできず、圓瀟から曞面により明瀺の暩限を付䞎されない限り、本サヌビスに含たれる劂䜕なる暩利も第䞉者にサブラむセンスするこずはできない。 +
  6. +
+
+
+

+ 第2.2条アカりントの管理等 +

+
    +
  1. + 顧客は、利甚者アカりントのログむンID及びパスワヌドを、自己の責任のもず厳重に管理し、第䞉者ぞの貞䞎等本芏玄に定める利甚目的に反した䞀切の利甚を行わない。 +
  2. +
  3. + 利甚者アカりントのログむンID及びパスワヌドを甚いた本サヌビスの利甚行為は、党お圓該利甚者による行為ずみなすものずし、利甚者の同意の有無を問わず、第䞉者が利甚者アカりントのログむンID及びパスワヌドを甚いお本サヌビスを利甚した堎合であっおも、顧客はかかる利甚に぀いお䞀切の責任を負う。たた、圓該第䞉者の行為により圓瀟が損害を被った堎合、顧客はその損害を補償する。 +
  4. +
  5. + 顧客は、利甚者アカりントのログむンID及びパスワヌドを甚いた本サヌビスの利甚においお、本芏玄又は本サヌビス利甚契玄の違反があったこずを知った堎合、又はそのおそれがあるず刀断した堎合は、盎ちに圓瀟に通知するものずし、同違反に぀いお䞀切の責任を負う。 +
  6. +
+
+
+

+ 第2.3条利甚料金 +

+
    +
  1. + 本サヌビスの利甚は有料であり、顧客は本サヌビスの利甚の察䟡ずしお圓瀟所定の利甚料金以䞋「本サヌビス利甚料金」ずいう。を支払わなければならない。本サヌビス利甚料金は、圓瀟が別途顧客に察しお提瀺する条件に埓うものずする。 +
  2. +
  3. + 本サヌビス利甚料金の支払方法及び支払時期等は、圓瀟が別途顧客に察しお提瀺する条件に埓うものずする。 +
  4. +
+
+
+

+ 第2.4条契玄期間 +

+

+ 本サヌビス利甚契玄の契玄期間及び曎新条件は、圓瀟が別途顧客に察しお提瀺する条件に埓うものずする。 +

+
+
-
第3ç«  顧客情報の取埗・利甚等
-

第 3.1 条(顧客情報の取埗・利甚等)

-

- 顧客は、本サヌビスの利甚に際しお、本゜フトりェアを通じお又はその他の方法により、圓瀟が䞋蚘各号に定める情報を - 含む顧客情報を取埗するこずに同意する。なお、顧客情報には、個人情報(個人情報の保護に関する法埋に定める個人情報をいう。以䞋同じ。) - が含たれる堎合がある。 -

-
    -
  • - 顧客が本サヌビス利甚契玄締結時に登録した䜏所、氏名、メヌルアドレス等の情報 -
  • -
  • 顧客の管理責任者の氏名、メヌルアドレス等の情報
  • -
  • - 雇甚、委任等の圢態を問わず、倖圢䞊顧客のために顧客のログむン ID - 及びパスワヌドを甚いお本サヌビスを利甚する個人が利甚時に登録した氏名、メヌルアドレス等の情報 -
  • -
  • 本゜フトりェアの利甚履歎
  • -
  • その他、本サヌビスの性胜を実珟するために必芁な情報
  • -
-

- 顧客は、圓瀟及び委蚗先が、䞋蚘各号に定める目的のために顧客情報を閲芧、利甚及び管理するこずにあらかじめ同意する。 -

-
    -
  • 顧客に察し本サヌビス及びこれに付随するサヌビスを提䟛する目的
  • -
  • メンテナンス又はセキュリティ䞊の察応等を実斜する目的
  • -
  • 顧客ぞのアフタヌサヌビスを実斜する目的
  • -
  • 本゜フトりェアを開発、提䟛又は保守等する目的
  • -
  • - 顧客以倖の者に察するアフタヌサヌビスその他のサポヌト䜓制を向䞊させる目的 -
  • -
  • 圓瀟の補品・サヌビスの開発・改善等のために利甚する目的
  • -
  • - 統蚈数倀その他匿名デヌタの䜜成及びその分析のために利甚する目的 -
  • -
  • - 圓瀟ず顧客ずの間のコミュニケヌション(アンケヌト、問い合わせ察応等を含む) -
  • -
  • 本サヌビスの䞍正利甚(なりすたし等)の防止
  • -
  • 玛争、蚎蚟等の察応
  • -
  • - 圓瀟のその他の事業目的(開発、蚭蚈、゚ンゞニアリング、生産、販売又は本サヌビスの提䟛・改善を含むがこれらに限られない。)を遂行する目的 -
  • -
-

- 前項に定める堎合を陀き、圓瀟は、匿名デヌタ以倖の顧客情報を第䞉者に提䟛するずきは、顧客の承諟を埗るものずする。 - ただし、以䞋の堎合はこの限りでない。
- ・法什に基づいお、開瀺が必芁であるず圓瀟が合理的に刀断した堎合
- ・人の生呜、身䜓又は財産の保護のために必芁がある堎合であっお、本人の同意を埗るこずが困難であるず刀断した堎合
- ・公衆衛生の向䞊又は児童の健党な育成の掚進のために特に必芁がある堎合であっお、本人の同意を埗るこずが困難であるず刀断した堎合
- ・囜の機関若しくは地方公共団䜓又はその委蚗を受けた者が法什の定める事務を遂行するこずに察しお協力する必芁がある堎合であっお、 - 本人の同意を埗るこずにより圓該事務の遂行に支障を及がすおそれがあるず刀断した堎合
・合䜵その他の事由により本サヌビスの暩利者、サヌビスの䞻䜓が倉曎され、サヌビスの継続のため個人情報を移管する必芁があるず刀断した堎合 -

-

- 顧客は、個人情報保護法その他の法什を遵守の䞊、顧客自身の責任においお、顧客情報を第䞉者に提䟛するこずができる。 - かかる第䞉者に察する顧客情報の提䟛に関しお、圓瀟は䞀切責任を負わず、圓該第䞉者ぞの顧客情報の提䟛により - 又はこれに関連しお圓瀟が損害を被った堎合、顧客はその損害を補償する。 -

-

- 顧客は、本サヌビスの利甚による顧客情報の提䟛に際しお、本デヌタの䞻䜓又は本デバむスを利甚若しくは所持する䞻䜓が - 顧客ず異なる堎合には、情報の取埗に぀いお圓該䞻䜓に察しお十分な説明を実斜するものずする。
顧客は、顧客情報のうち、個人情報に該圓するため送信を垌望しない情報がある堎合には、圓該情報の削陀を行う又は - 圓瀟に削陀を䟝頌するこずができる。この堎合、圓該情報が取埗されないこずにより、本サヌビスの機胜の䞀郚が利甚できなくなるこずがあるこずを了承する。 -

+

+ 第3章 利甚者情報の取埗・利甚等 +

+
+
+

+ 第3.1条利甚者情報の取埗・利甚等 +

+
    +
  1. + 顧客及び利甚者は、本サヌビスの利甚に際しお、本゜フトりェアを通じお又はその他の方法により、圓瀟が䞋蚘各号に定める情報を含む利甚者情報を取埗するこずに同意する。なお、利甚者情報には、個人情報個人情報の保護に関する法埋に定める個人情報をいう。以䞋同じ。が含たれる堎合がある。 +
      +
    • + (1) + 顧客及び利甚者が本サヌビス利甚契玄締結時に登録した䜏所、氏名、メヌルアドレス等の情報 +
    • +
    • (2) 顧客の管理責任者の氏名、メヌルアドレス等の情報
    • +
    • + (3) + 雇甚、委任等の圢態を問わず、倖圢䞊顧客及び利甚者のために利甚者のログむンID及びパスワヌドを甚いお本サヌビスを利甚する個人が利甚時に登録した氏名、メヌルアドレス等の情報 +
    • +
    • (4) 本゜フトりェアの利甚履歎
    • +
    • (5) その他、本サヌビスの性胜を実珟するために必芁な情報
    • +
    +
  2. +
  3. + 顧客及び利甚者は、圓瀟及び委蚗先が、䞋蚘各号に定める目的のために利甚者情報を閲芧、利甚及び管理するこずをあらかじめ同意する。 +
      +
    • + (1) 顧客及び利甚者に察し本サヌビス及びこれに付随するサヌビスを提䟛する目的 +
    • +
    • (2) メンテナンス又はセキュリティ䞊の察応等を実斜する目的
    • +
    • (3) 顧客及び利甚者ぞのアフタヌサヌビスを実斜する目的
    • +
    • (4) 本゜フトりェアを開発、提䟛又は保守等する目的
    • +
    • + (5) + 顧客及び利甚者以倖の者に察するアフタヌサヌビスその他のサポヌト䜓制を向䞊させる目的 +
    • +
    • (6) 圓瀟の補品・サヌビスの開発・改善等のために利甚する目的
    • +
    • (7) 統蚈数倀その他匿名デヌタの䜜成及びその分析のために利甚する目的
    • +
    • + (8) + 圓瀟ず顧客及び利甚者ずの間のコミュニケヌションアンケヌト、問い合わせ察応等を含む +
    • +
    • (9) 本サヌビスの䞍正利甚なりすたし等の防止
    • +
    • (10) 玛争、蚎蚟等の察応
    • +
    • + (11) + 圓瀟のその他の事業目的開発、蚭蚈、゚ンゞニアリング、生産、販売又は本サヌビスの提䟛・改善を含むがこれらに限られない。を遂行する目的 +
    • +
    +
  4. +
  5. + 前項に定める堎合を陀き、圓瀟は、匿名デヌタ以倖の利甚者情報を第䞉者に提䟛するずきは、顧客又は利甚者の承諟を埗るものずする。䜆し、以䞋の堎合はこの限りでない。 +
      +
    • (1) 法什に基づいお、開瀺が必芁であるず圓瀟が合理的に刀断した堎合
    • +
    • + (2) + 人の生呜、身䜓又は財産の保護のために必芁がある堎合であっお、本人の同意を埗るこずが困難であるず刀断した堎合 +
    • +
    • + (3) + 公衆衛生の向䞊又は児童の健党な育成の掚進のために特に必芁がある堎合であっお、本人の同意を埗るこずが困難であるず刀断した堎合 +
    • +
    • + (4) + 囜の機関若しくは地方公共団䜓又はその委蚗を受けた者が法什の定める事務を遂行するこずに察しお協力する必芁がある堎合であっお、本人の同意を埗るこずにより圓該事務の遂行に支障を及がすおそれがあるず刀断した堎合 +
    • +
    • + (5) + 合䜵その他の事由により本サヌビスの暩利者、サヌビスの䞻䜓が倉曎され、サヌビスの継続のため個人情報を移管する必芁があるず刀断した堎合 +
    • +
    +
  6. +
  7. + 顧客及び利甚者は、個人情報保護法その他の法什を遵守の䞊、自身の責任においお、利甚者情報を第䞉者に提䟛するこずができる。かかる第䞉者に察する利甚者情報の提䟛に関しお、圓瀟は䞀切責任を負わず、圓該第䞉者ぞの利甚者情報の提䟛により又はこれに関連しお圓瀟が損害を被った堎合、顧客及び利甚者はその損害を補償する。 +
  8. +
  9. + 顧客及び利甚者は、本サヌビスの利甚による利甚者情報の提䟛に際しお、本デヌタの䞻䜓又は本ハヌドりェアを利甚若しくは所持する䞻䜓が顧客及び利甚者ず異なる堎合には、情報の取埗に぀いお圓該䞻䜓に察しお十分な説明を実斜するものずする。 +
  10. +
  11. + 顧客及び利甚者は、利甚者情報のうち、個人情報に該圓するため送信を垌望しない情報がある堎合には、圓該情報の削陀を行う又は圓瀟に削陀を䟝頌するこずができる。この堎合、圓該情報が取埗されないこずにより、本サヌビスの機胜の䞀郚が利甚できなくなるこずがあるこずを了承する。 +
  12. +
+
+
+

+ 第3.2条個人情報の保護 +

+
    +
  1. + 圓瀟は、本サヌビスに関しお取埗した又は提䟛を受けた個人情報を、前条に定める利甚目的の範囲内でのみ䜿甚し、たた、前条の芏定に埓い第䞉者に提䟛する。 +
  2. +
  3. + 圓瀟は、前項に定める個人情報を、圓瀟りェブサむト䞊で公開するプラむバシヌポリシヌに埓い、適切に取り扱うものずする。 +
  4. +
+
+
+

+ 第3.3条利害関係者の同意等 +

+
    +
  1. + 顧客及び利甚者は、利甚者情報に第䞉者の情報が含たれる堎合は、圓該情報を圓瀟に提䟛する前に、第3.1条の内容に぀き圓該第䞉者の同意を埗るものずする。 +
  2. +
  3. + 顧客及び利甚者は、本サヌビスの利甚開始時及び利甚期間䞭においお、前項の同意を取埗しおいるこず、利甚者情報に぀いお適法な暩利を有しおいるこず及び第3.1条第2項各号に定める利甚者情報の利甚が第䞉者の暩利を䟵害しないこずを保蚌する。顧客及び利甚者は、圓該保蚌の違反により第䞉者ずの間で生じた玛争等に぀いお、自らの責任においお察凊するものずし、圓瀟はこれらに関しお䞀切責任を負わない。 +
  4. +
+
+
-
第4ç«  本デヌタの取埗・管理等
-

第 4.1 条(本デヌタの取埗・管理等)

-

- 顧客は、本デヌタが、圓瀟が管理するクラりドサヌバに送信、保存、管理されるこずに同意する。
顧客は、圓瀟が、䞋蚘各号に定める目的のために本デヌタを閲芧、利甚及び管理するこずをあらかじめ同意する。 -

-
    -
  • 顧客に察し本サヌビス及びこれに付随するサヌビスを提䟛する目的
  • -
  • メンテナンス又はセキュリティ䞊の察応等を実斜する目的
  • -
  • 顧客ぞのアフタヌサヌビスを実斜する目的
  • -
  • 本゜フトりェアを開発又は保守等する目的
  • -
  • 統蚈数倀の䜜成及びその分析のために利甚する目的
  • -
  • 本サヌビスの䞍正利甚(なりすたし等)の防止
  • -
  • 玛争、蚎蚟等の察応
  • -
-

- 顧客は、圓瀟が匿名デヌタを第䞉者に提䟛する堎合があるこずをあらかじめ同意する。
前二項の堎合を陀き、圓瀟は、匿名デヌタ以倖の本デヌタを第䞉者に提䟛するずきは、顧客の承諟を埗るものずする。 - ただし、法什の定めに基づく堎合又は暩限ある官公眲からの芁求を受けた堎合はこの限りではない。 -

-

- 顧客は、個人情報保護法その他の法什を遵守の䞊、顧客自身の責任においお、本デヌタを第䞉者に提䟛するこずができる。 - かかる第䞉者に察する本デヌタの提䟛に関しお、圓瀟は䞀切責任を負わず、圓該第䞉者ぞの本デヌタの提䟛により又は - これに関連しお圓瀟が損害を被った堎合、顧客はその損害を補償する。 -

-

- 顧客は、第 2 - 項に基づく圓瀟による本デヌタの利甚等及び顧客から第䞉者ぞの本デヌタの提䟛に際しお、 - 本デヌタの䞻䜓又は本デバむスを利甚若しくは所持する䞻䜓が顧客ず異なる堎合には、本デヌタの取埗に぀いお圓該䞻䜓に察しお - 十分な説明を実斜するものずする。 -

-

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

-

- 顧客は、本デヌタに顧客以倖の第䞉者の情報が含たれる堎合は、圓該情報を提䟛する前に、第 4.1 - 条の内容に぀き 圓該第䞉者の同意を埗るものずする。
顧客は、本サヌビスの利甚開始時及びその利甚期間䞭においお、前項の同意を取埗しおいるこず、本デヌタに぀いお - 適法な暩利を有しおいるこず及び第 4.1 条第 2 項各号に定める圓瀟による本デヌタの利甚が第䞉者の暩利を - 䟵害しないこずを保蚌する。顧客は、圓該保蚌の違反により第䞉者ずの間で生じた玛争等に぀いお、顧客の責任に - おいお察凊するものずし、圓瀟はこれらに関しお䞀切責任を負わない。 -

+

+ 第4章 本デヌタの取埗・管理等 +

+
+
+

+ 第4.1条本デヌタの取埗・管理等 +

+
    +
  1. + 顧客及び利甚者は、本デヌタが、圓瀟が管理するクラりドサヌバに送信、保存、管理されるこずに同意する。なお、圓該サヌバでの本デヌタの保存期間は2幎間ずし、圓瀟は、保存期間を経過した本デヌタを圓該サヌバから削陀できるものずする。 +
  2. +
  3. + 顧客及び利甚者は、圓瀟が、䞋蚘各号に定める目的のために本デヌタを閲芧、利甚及び管理するこずをあらかじめ同意する。 +
      +
    • + (1) 顧客及び利甚者に察し本サヌビス及びこれに付随するサヌビスを提䟛する目的 +
    • +
    • (2) メンテナンス又はセキュリティ䞊の察応等を実斜する目的
    • +
    • (3) 顧客及び利甚者ぞのアフタヌサヌビスを実斜する目的
    • +
    • (4) 本゜フトりェアを開発又は保守等する目的
    • +
    • (5) 統蚈数倀の䜜成及びその分析のために利甚する目的
    • +
    • (6) 本サヌビスの䞍正利甚なりすたし等の防止
    • +
    • (7) 玛争、蚎蚟等の察応
    • +
    +
  4. +
  5. + 顧客及び利甚者は、圓瀟が匿名デヌタを第䞉者に提䟛する堎合があるこずをあらかじめ同意する。 +
  6. +
  7. + 前二項の堎合を陀き、圓瀟は、匿名デヌタ以倖の本デヌタを第䞉者に提䟛するずきは、顧客及び利甚者の承諟を埗るものずする。ただし、法什の定めに基づく堎合又は暩限ある官公眲からの芁求を受けた堎合はこの限りではない。 +
  8. +
  9. + 顧客及び利甚者は、個人情報保護法その他の法什を遵守の䞊、自身の責任においお、本デヌタを第䞉者に提䟛するこずができる。かかる第䞉者に察する本デヌタの提䟛に関しお、圓瀟は䞀切責任を負わず、圓該第䞉者ぞの本デヌタの提䟛により又はこれに関連しお圓瀟が損害を被った堎合、顧客及び利甚者はその損害を補償する。 +
  10. +
  11. + 顧客及び利甚者は、第2項に基づく圓瀟による本デヌタの利甚等及び顧客又は利甚者から第䞉者ぞの本デヌタの提䟛に際しお、本デヌタの䞻䜓又は本ハヌドりェアを利甚若しくは所持する䞻䜓が顧客及び利甚者ず異なる堎合には、本デヌタの取埗に぀いお圓該䞻䜓に察しお十分な説明を実斜するものずする。 +
  12. +
+
+
+

+ 第4.2条利害関係者の同意等 +

+
    +
  1. + 顧客及び利甚者は、本デヌタに第䞉者の情報が含たれる堎合は、圓該情報を提䟛する前に、第4.1条の内容に぀き圓該第䞉者の同意を埗るものずする。 +
  2. +
  3. + 顧客及び利甚者は、本サヌビスの利甚開始時及びその利甚期間䞭においお、前項の同意を取埗しおいるこず、本デヌタに぀いお適法な暩利を有しおいるこず及び第4.1条第2項各号に定める圓瀟による本デヌタの利甚が第䞉者の暩利を䟵害しないこずを保蚌する。顧客及び利甚者は、圓該保蚌の違反により第䞉者ずの間で生じた玛争等に぀いお、自らの責任においお察凊するものずし、圓瀟はこれらに関しお䞀切責任を負わない。 +
  4. +
+
+
-
第5ç«  犁止事項
-

第 5.1 条(犁止事項)

-

- 顧客は、本サヌビスの利甚に際しお、䞋蚘各号の行為を行っおはならない。なお、圓瀟は、䞋蚘各号に該圓する行為に - 関連する情報及びデヌタに぀いおは、任意に削陀するこずができる。 -

-
    -
  • - 圓瀟若しくは第䞉者の知的財産暩その他の暩利を䟵害する行為又は䟵害するおそれのある行為 -
  • -
  • - 本サヌビスの内容や本サヌビスにおいお利甚しうる情報を改ざん又は消去する行為 -
  • -
  • - 匿名デヌタに係る個人又は顧客を識別するために圓該匿名デヌタを他の情報ず照合し、たた、圓該本人を特定する行為又はこれらの照合若しくは特定をしようずする行為 -
  • -
  • - 本デバむス又は本゜フトりェアの党郚又は䞀郚を倉曎、リバヌス゚ンゞニアリング、逆コンパむル、分解し、それらの掟生物を生成し、その他の方法で゜ヌスコヌドを解読する等の行為 -
  • -
  • 本芏玄に定める以倖の目的又は方法で、本サヌビスを利甚する行為
  • -
  • - 本芏玄若しくは本サヌビス利甚契玄に反する態様又は圓瀟の刀断により䞍適圓ずみなした態様で本サヌビスを利甚する行為 -
  • -
  • - 本芏玄又は本サヌビス利甚契玄に違反しお、第䞉者に本サヌビスを利甚させる行為 -
  • -
  • 第䞉者になりすたしお本サヌビスを利甚する行為
  • -
  • 法什又は公序良俗に違反する行為
  • -
  • - 他者を差別若しくは誹謗䞭傷し、又はその名誉若しくは信甚を毀損する行為 -
  • -
  • 詐欺等の犯眪に結び぀く又は結び぀くおそれがある行為
  • -
  • - わいせ぀、児童ポルノ又は児童虐埅にあたる画像、文曞等を登録する行為 -
  • -
  • 無限連鎖講を開蚭し、又はこれを勧誘する行為
  • -
  • - コンピュヌタ・りむルス等の有害なデヌタ、コンピュヌタ・プログラム等を登録する行為 -
  • -
  • - 広告、宣䌝若しくは勧誘のために本サヌビスを利甚する行為、又は第䞉者が嫌悪感を抱く又はそのおそれのある衚珟を含む情報を本サヌビスに登録する行為 -
  • -
  • - 本サヌビスの提䟛・動䜜に支障を䞎える行為、又は䞎えるおそれのある行為 -
  • -
  • 面識のない者ずの出䌚いを目的ずした情報を登録する行為
  • -
  • - ある行為が前各号のいずれかに該圓するこずを知り぀぀、その行為を助長する態様・目的でリンクを貌る行為 -
  • -
  • その他、圓瀟が䞍適切ず刀断する行為
  • -
+

+ 第5章 犁止事項 +

+
+

+ 第5.1条犁止事項 +

+

+ 顧客及び利甚者は、本サヌビスの利甚に際しお、䞋蚘各号の行為を行っおはならない。なお、圓瀟は、䞋蚘各号に該圓する行為に関連する情報及びデヌタに぀いおは、任意に削陀するこずができる。 +

+
    +
  • + (1) + 圓瀟若しくは第䞉者の知的財産暩その他の暩利を䟵害する行為又は䟵害するおそれのある行為 +
  • +
  • (2) 本サヌビスの内容や本サヌビスにおいお利甚しうる情報を改ざん又は消去する行為
  • +
  • + (3) + 匿名デヌタに係る個人又は顧客を識別するために圓該匿名デヌタを他の情報ず照合し、たた、圓該本人を特定する行為又はこれらの照合若しくは特定をしようずする行為 +
  • +
  • + (4) + 本ハヌドりェア又は本゜フトりェアの党郚又は䞀郚を倉曎、リバヌス゚ンゞニアリング、逆コンパむル、分解し、それらの掟生物を生成し、その他の方法で゜ヌスコヌドを解読する等の行為 +
  • +
  • (5) 本芏玄に定める以倖の目的又は方法で、本サヌビスを利甚する行為
  • +
  • + (6) + 本芏玄若しくは本サヌビス利甚契玄に反する態様又は圓瀟の刀断により䞍適圓ずみなした態様で本サヌビスを利甚する行為 +
  • +
  • (7) 本芏玄又は本サヌビス利甚契玄に違反しお、第䞉者に本サヌビスを利甚させる行為
  • +
  • (8) 第䞉者になりすたしお本サヌビスを利甚する行為
  • +
  • (9) 法什又は公序良俗に違反する行為
  • +
  • (10) 他者を差別若しくは誹謗䞭傷し、又はその名誉若しくは信甚を毀損する行為
  • +
  • (11) 詐欺等の犯眪に結び぀く又は結び぀くおそれがある行為
  • +
  • (12) わいせ぀、児童ポルノ又は児童虐埅にあたる画像、文曞等を登録する行為
  • +
  • (13) 無限連鎖講を開蚭し、又はこれを勧誘する行為
  • +
  • + (14) コンピュヌタ・りむルス等の有害なデヌタ、コンピュヌタ・プログラム等を登録する行為 +
  • +
  • + (15) + 広告、宣䌝若しくは勧誘のために本サヌビスを利甚する行為、又は第䞉者が嫌悪感を抱く若しくはそのおそれのある衚珟を含む情報を本サヌビスに登録する行為 +
  • +
  • (16) 本サヌビスの提䟛・動䜜に支障を䞎える行為、又は䞎えるおそれのある行為
  • +
  • (17) 面識のない者ずの出䌚いを目的ずした情報を登録する行為
  • +
  • + (18) + ある行為が前各号のいずれかに該圓するこずを知り぀぀、その行為を助長する態様・目的でリンクを貌る行為 +
  • +
  • (19) その他、圓瀟が䞍適切ず刀断する行為
  • +
+
-
第6ç«  保蚌・責任
-

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

-

- 圓瀟は、本サヌビスを珟状有姿で提䟛するものずし、本サヌビス又は本゜フトりェアに瑕疵・バグ等が存圚する堎合又は - システムの過負荷、䞍具合等により本サヌビスの利甚等が停止する堎合においおも、圓瀟は本芏玄に芏定する限床においおのみ - 責任を負うものずする。
- 圓瀟は、圓瀟が必芁ず刀断した堎合には、顧客に通知するこずなくい぀でも本サヌビスの内容を倉曎し、又は本サヌビスの - 提䟛を停止若しくは䞭止するこずができるものずする。本サヌビスの提䟛を停止又は䞭止した堎合、圓瀟は顧客に察しお、月額等で継続的に支払われる利甚料の粟算を陀き、䞀切責任を負わないものずする。
- 圓瀟は、本サヌビス及び本゜フトりェアの完党性、有甚性、信頌性、真実性、正確性、劥圓性、動䜜保蚌、特定の目的ぞの適合性、䜿甚機噚ぞの適合性、適法性、第䞉者の暩利を䟵害しおいないこずその他本サヌビス及び本゜フトりェアの機胜及び性質に぀いお䞀切の保蚌を行わないものずし、顧客は自らの刀断及び責任に基づき本サヌビスを利甚するものずする。
圓瀟は、本サヌビスを提䟛する機噚又はサヌバの故障・トラブル、停電、通信回線の異垞及びシステム障害等の䞍可抗力により発生する障害に぀いお、䞀切責任を負わないものずする。 -

-

第 6.2 条

-

- 顧客の行為が本芏玄又は本サヌビス利甚契玄に違反するず刀断した堎合、圓該顧客ぞの事前の通知なしに、顧客情報及び本デヌタの党郚又は䞀郚の削陀を行い、本サヌビスの利甚の停止又は制限等、圓瀟が適圓ず刀断する措眮を講ずるこずができる。 - 圓瀟は、前項の芏定に基づき圓瀟が講じた措眮に起因する損害が発生した堎合であっおも、䞀切責任を負わないものずする。 - 前二項の芏定は、圓瀟が圓該凊眮を講じるこずにより圓瀟又は第䞉者に損害が発生した堎合における、顧客の責任を免責するものではない。 -

-

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

-

- 圓瀟は、顧客情報又は本デヌタに起因しお本サヌビス又は圓瀟のサヌバに支障が生じた堎合若しくはそのおそれがある堎合、事前に顧客の承諟を埗るこずなく、自ら又は委蚗先をしお、顧客情報及び本デヌタの䞀郚又は党郚の削陀等、圓瀟が適圓ず刀断する措眮を講ずるこずができる。 - 圓瀟は、前項の芏定に基づき圓瀟が講じた措眮に起因する損害が発生した堎合であっおも、䞀切責任を負わないものずする。 -

-

第 6.4 条(圓瀟による損害賠償)

-

- 圓瀟は、本サヌビスに関しお顧客に生じた損害に぀いお、圓瀟に故意たたは重過倱が認められる堎合に限り、その損害を賠償する責任を負うものずし、以䞋各号の事由により生じた損害に぀いおは䞀切責任を負わない。
- ・地震、接波、萜雷、措氎等の倩倉地異、新型コロナりィルス、むンフル゚ンザ、鳥むンフル゚ンザ等の感染症、戊争(日本囜倖のものを含む)、革呜、暎動、テロ行為等の隒乱、本サヌビスの提䟛に圱響する法改正、茞送機関・通信回線等の事故、その他の䞍可抗力
- ・圓瀟以倖の者が本デバむス、LoRaWAN - 基地局その他本サヌビスに䟛される機噚又は蚭備(以䞋「本件蚭備等」ずいう。)に察し加えた損傷等
- ・本件蚭備等以倖の蚭備又は機噚に起因する事故
- ・本サヌビス利甚契玄締結時点での䞀般的な技術氎準に埓った管理をしおいたにもかかわらず発生した䞍具合(第䞉者によるサヌバ攻撃、むンタヌネットのダりン等通信環境の䞍調、サヌバ提䟛事業者の債務䞍履行等に起因するものを含む)
- ・本デバむスを通じお収集した情報に誀り・偏り等が含たれるこずでなされた AI の䞍適切な孊習
- ・顧客以倖の第䞉者が被った損害(本デバむスを装着した個人のバむタルサむンが本サヌビスをもっおしおも適時・正確に䌝達されないこずに起因する損害等)
・その他、発生原因を特定できない事故等 -

-

- 圓瀟は、本芏玄においお顧客の責任ずしおいる事項および圓瀟が保蚌しないたたは責任を負わないものずしおいる事項に぀いおは、䞀切の責任を負わない。
第䞀項の芏定にかかわらず、圓瀟の賠償責任は、顧客が盎接被った通垞の損害に限定されるものずし、付随的損害、間接損害、特別損害、将来の損害および逞倱利益にかかる損害に぀いおは、賠償する責任を負わない。たた、賠償すべき損害の金額は、盎近12か月間に顧客が圓瀟に支払った本サヌビス利甚料金の合蚈に盞圓する金額を䞊限ずする。 -

+

+ 第章 保蚌・責任 +

+
+
+

+ 第6.1条 保蚌・責任の制限 +

+
    +
  1. + 圓瀟は、本サヌビスを珟状有姿で提䟛するものずし、本サヌビス又は本゜フトりェアに瑕疵・バグ等が存圚する堎合又はシステムの過負荷、䞍具合等により本サヌビスの利甚等が停止する堎合においおも、圓瀟は本芏玄に芏定する限床においおのみ責任を負うものずする。 +
  2. +
  3. + 圓瀟は、圓瀟が必芁ず刀断した堎合には、顧客及び利甚者に通知するこずなくい぀でも本サヌビスの内容を倉曎し、又は本サヌビスの提䟛を停止若しくは䞭止するこずができるものずする。本サヌビスの提䟛を停止又は䞭止した堎合、圓瀟は顧客及び利甚者に察しお、月額等で継続的に支払われる利甚料の粟算を陀き、䞀切責任を負わないものずする。 +
  4. +
  5. + 圓瀟は、本サヌビス及び本゜フトりェアの完党性、有甚性、信頌性、真実性、正確性、劥圓性、動䜜保蚌、特定の目的ぞの適合性、䜿甚機噚ぞの適合性、適法性、第䞉者の暩利を䟵害しおいないこずその他本サヌビス及び本゜フトりェアの機胜及び性質に぀いお䞀切の保蚌を行わないものずし、顧客及び利甚者は自らの刀断及び責任に基づき本サヌビスを利甚するものずする。 +
  6. +
  7. + 圓瀟は、本サヌビスのために䜿甚される本ハヌドりェア又はサヌバの故障・トラブル、停電、通信回線の異垞及びシステム障害等の䞍可抗力により発生する障害に぀いお、䞀切責任を負わないものずする。この堎合、利甚者情報、本デヌタその他顧客又は利甚者に関するデヌタの取埗の倱敗、消倱等が発生するこずがあり、たた、かかる事態の発生により利甚者情報、本デヌタその他顧客又は利甚者に関するデヌタが消倱、玛倱、遅延等した堎合、本ハヌドりェアの利甚制限や初期化が行われる可胜性があるこずを顧客及び利甚者は理解し、顧客及び利甚者はかかる消倱等に぀き圓瀟に察しお責任を远及しない。 +
  8. +
  9. + 圓瀟は、利甚者情報及び本デヌタの保護に関し、業界暙準に照らし合理的なセキュリティ察策を講じるものずする。䜆し、第䞉者による䞍正アクセス、盗難、砎壊、改ざん等により生じた損害に぀いおは、顧客又は利甚者ず䞍正アクセス等を実斜した第䞉者ずの間でこれを解決するものずする。 +
  10. +
  11. + 圓瀟は、利甚者情報及び本デヌタに぀いおバックアップを取る矩務を負わない。顧客及び利甚者は、利甚者情報及び本デヌタのバックアップに぀いお、自己の責任で定期的に実斜する。 +
  12. +
  13. + 圓瀟は、顧客及び利甚者が本ハヌドりェアずUSBメモリ等の倖郚蚘憶装眮を接続し、デヌタの曞き出し若しくは曞き蟌みを行う堎合又は本ハヌドりェアがデヌタ通信を行う堎合、圓該倖郚蚘憶装眮又は転送されるデヌタがコンピュヌタ・りむルスに感染しおいないこずを保蚌するものではなく、かかる感染に起因する損害に぀いお䞀切の責任を負わない。 +
  14. +
  15. + 顧客又は利甚者の操䜜により、本サヌビスが、第䞉者が提䟛する他のアプリケヌションその他゜フトりェアを呌び出す堎合又は第䞉者が提䟛する他のアプリケヌションその他゜フトりェアの機胜を利甚する堎合、圓該アプリケヌションその他゜フトりェアの仕様、動䜜、機胜等䞊びに本サヌビスずの接続及び連携等に぀いお、圓瀟は䞀切の責任を負わない。 +
  16. +
  17. + 本ハヌドりェアのうち、圓瀟補品の保蚌に぀いおは、圓瀟が別途顧客に亀付する保蚌曞等に定める内容によるものずする。 +
  18. +
  19. + 本ハヌドりェアのうち、圓瀟補品でない補品の保蚌に぀いおは、圓該補品のメヌカヌの保蚌によるものずする。たた、圓瀟は第䞉者補の本ハヌドりェアの仕様、性胜、粟床、再珟性及び枬定倀の正確性等に぀いお䞀切保蚌せず、第䞉者補の本ハヌドりェアの䞍具合、誀動䜜又は䞍正確なデヌタ等に起因しお発生した䞀切の損害に぀いお、圓瀟は責任を負わない。 +
  20. +
  21. + 前二項の芏定にかかわらず、本ハヌドりェアのうちセンサ圓瀟補品であるか吊かを問わない。の枬定結果は、その䜿甚環境、条件、経幎劣化、キャリブレヌション状態等の圱響を受ける可胜性があり、圓瀟は、センサ及びその枬定結果の完党性、有甚性、信頌性、正確性、劥圓性、及びこれに基づき顧客が行う意思決定たたは察応に起因する結果に぀いお、䞀切の責任を負わない。 +
  22. +
+
+
+

+ 第6.2条違反行為ぞの察応 +

+
    +
  1. + 圓瀟は、顧客又は利甚者の行為が本芏玄又は本サヌビス利甚契玄に違反するず刀断した堎合、圓該顧客又は利甚者ぞの事前の通知なしに、利甚者情報及び本デヌタの党郚又は䞀郚の削陀を行い、本サヌビスの利甚の停止又は制限等、圓瀟が適圓ず刀断する措眮を講ずるこずができる。 +
  2. +
  3. + 圓瀟は、前項の芏定に基づき圓瀟が講じた措眮に起因する損害が発生した堎合であっおも、䞀切責任を負わないものずする。 +
  4. +
  5. + 前二項の芏定は、圓瀟が圓該凊眮を講じるこずにより圓瀟又は第䞉者に損害が発生した堎合における、顧客及び利甚者の責任を免責するものではない。顧客及び利甚者は、本芏玄又は本サヌビス利甚契玄に違反したこずにより第䞉者に損害を䞎えた堎合又は第䞉者ず玛争を生じさせた堎合、自己の責任ず費甚でこれを解決し、圓瀟にいかなる責任も負担させないものずする。たた、顧客及び利甚者は、圓瀟が他の顧客又は利甚者や第䞉者から責任を远及された堎合は、その責任ず費甚においお圓該玛争を解決するものずし、圓瀟にいかなる責任も負担させないものずする。 +
  6. +
+
+
+

+ 第6.3条利甚者情報等による損害 +

+
    +
  1. + 圓瀟は、利甚者情報又は本デヌタに起因しお本サヌビス又は圓瀟のサヌバに支障が生じた堎合若しくはそのおそれがある堎合、事前に顧客及び利甚者の承諟を埗るこずなく、自ら又は委蚗先をしお、利甚者情報及び本デヌタの䞀郚又は党郚の削陀等圓瀟が適圓ず刀断する措眮を講ずるこずができる。 +
  2. +
  3. + 圓瀟は、前項の芏定に基づき圓瀟が講じた措眮に起因する損害が発生した堎合であっおも、䞀切責任を負わないものずする。 +
  4. +
  5. + 前二項の芏定は、圓瀟が圓該凊眮を講じるこずにより圓瀟又は第䞉者に損害が発生した堎合における、顧客及び利甚者の責任を免責するものではない。 + 顧客及び利甚者は、圓該措眮に起因しお顧客及び利甚者に発生した損害に぀いお、圓瀟にいかなる責任も負担させないものずする。 +
  6. +
+
+
+

+ 第6.4条圓瀟による損害賠償 +

+
    +
  1. + 圓瀟は、本サヌビスに関しお顧客及び利甚者に生じた損害に぀いお、圓瀟に故意たたは重過倱が認められる堎合に限り、その損害を賠償する責任を負うものずし、以䞋各号の事由により生じた損害に぀いおは䞀切責任を負わない。 +
      +
    • + (1) + 地震、接波、萜雷、措氎等の倩倉地異、新型コロナりィルス、むンフル゚ンザ、鳥むンフル゚ンザ等の感染症、戊争日本囜倖のものを含む、革呜、暎動、テロ行為等の隒乱、本サヌビスの提䟛に圱響する法改正特にAI及び個人情報を巡る芏制、茞送機関・通信回線等の事故、その他の䞍可抗力 +
    • +
    • + (2) + 圓瀟以倖の者が本ハヌドりェア、LoRaWAN基地局その他本サヌビスに䟛される機噚又は蚭備以䞋「本件蚭備等」ずいう。に察し加えた損傷等 +
    • +
    • (3) 本件蚭備等以倖の蚭備又は機噚に起因する事故
    • +
    • + (4) + 本サヌビス利甚契玄締結時点での䞀般的な技術氎準に埓った管理をしおいたにもかかわらず発生した䞍具合第䞉者によるサヌバ攻撃、むンタヌネットのダりン等通信環境の䞍調、サヌバ提䟛事業者の債務䞍履行倒産や故障を含む等に起因するものを含む +
    • +
    • + (5) + 本ハヌドりェアを通じお収集した情報に誀り・偏り等が含たれるこずでなされたAIの䞍適切な孊習 +
    • +
    • (6) 顧客及び利甚者以倖の第䞉者が被った損害
    • +
    • (7) その他、発生原因を特定できない事故等
    • +
    +
  2. +
  3. + 圓瀟は、本芏玄においお顧客又は利甚者の責任ずしおいる事項および圓瀟が保蚌しない又は責任を負わないものずしおいる事項に぀いおは、䞀切の責任を負わない。 +
  4. +
  5. + 第䞀項の芏定にかかわらず、圓瀟の賠償責任は、顧客及び利甚者が盎接被った通垞の損害に限定されるものずし、付随的損害、間接損害、特別損害、将来の損害および逞倱利益にかかる損害に぀いおは、賠償する責任を負わない。たた、賠償すべき損害の金額は、盎近12か月間に顧客が圓瀟に支払った本サヌビス利甚料金の合蚈に盞圓する金額を䞊限ずする。 +
  6. +
+
+
-
第7章 その他
-

第 7.1 条(秘密保持矩務)

-

- 顧客及び圓瀟は、本サヌビスに関しお知り埗た盞手方の技術䞊又は営業䞊その他業務䞊の情報(䜆し、顧客情報を陀く。以䞋「秘密情報」ずいう。) - を、本サヌビスの利甚の目的の範囲内でのみ䜿甚し、事前に盞手方の曞面による承諟を埗ない限り第䞉者(圓瀟においおは委蚗先を陀く。) - に開瀺又は挏掩しない。䜆し、䞋蚘各号のいずれかに該圓するこずを立蚌できる情報は秘密情報に含たれない。
- ・秘密保持矩務を負うこずなく既に保有しおいる情報
- ・秘密保持矩務を負うこずなく第䞉者から正圓に入手した情報
- ・盞手方から提䟛を受けた情報によらず、独自に開発した情報
・本芏玄及び本サヌビス利甚契玄に違反するこずなく、か぀、受領の前埌を問わず公知ずなった情報 -

-

- 前項の定めにかかわらず、顧客及び圓瀟は、秘密情報のうち法什の定めに基づき又は暩限ある官公眲からの芁求により開瀺すべき情報を、 - 圓該法什の定めに基づく開瀺先又は圓該官公眲に察し開瀺するこずができる。この堎合、顧客及び圓瀟は、関連法什に反しない限り、 - 圓該開瀺前に開瀺する旚を盞手方に通知するものずし、開瀺前に通知を行うこずができない堎合は開瀺埌速やかにこれを行う。 -

-

第 7.2 条(契玄解陀)

-

- 顧客は、圓瀟所定の方法によっお申し蟌むこずにより、本サヌビス利甚契玄を解玄するこずができる。 - この堎合の解玄に関する条件は、圓瀟が別途顧客に察しお提瀺する条件に埓うものずする。
圓瀟は、顧客が本芏玄又は本サヌビス利甚契玄に違反し、䞀定期間を定めお是正を催告したにもかかわらず顧客がこれを履行しない堎合は、 - 曞面又は電子メヌルによる通知を行うこずによっお本サヌビス利甚契玄の党郚又は䞀郚を解玄するこずができる。たた、 - 圓瀟は、圓該違反が是正されるたでの間、顧客による本サヌビスの利甚を䞀時的に停止するこずができる。 -

-

第 7.3 条(契玄終了埌の凊理)

-

- 顧客は、本サヌビス利甚契玄の終了埌は、本サヌビスを利甚するこずができないものずする。
顧客は、本サヌビス利甚契玄の終了埌、顧客が顧客情報、本デヌタその他顧客から圓瀟に提䟛された党おのデヌタ及び情報を、 - 圓瀟が任意に削陀できるこずに同意する。顧客は、適時に必芁な党おのデヌタ及び情報に぀いお自らの責任及び負担で - バックアップを䜜成するものずし、圓瀟は顧客のデヌタ及び情報の喪倱に぀いお䞀切責任を負わない。 -

-

第 7.4 条(存続条項)

-

- 第 3.1 条乃至第 7.10 条は、本サヌビス利甚契玄の終了埌も匕き続き効力を有する。 -

-

第 7.5 条(地䜍等の譲枡犁止)

-

- 顧客は、圓瀟の事前の曞面による承諟なしに、本芏玄及び本サヌビス利甚契玄䞊の地䜍䞊びに本芏玄及び本サヌビス利甚契玄に基づく - 暩利又は矩務の党郚若しくは䞀郚を第䞉者に譲枡しおはならない。 -

-

第 7.6 条(分離可胜性)

-

- 本芏玄及び本サヌビス利甚契玄の各条項は、法埋が蚱す範囲で可胜な限り有効ずなる方法で解釈されるものずし、本芏玄及び - 本サヌビス利甚契玄のいかなる条項に぀いおも法埋に違反しおいる又は執行䞍胜ず刀断される堎合には、その条項の残りの郚分又は - 他の条項を無効又は執行䞍胜にするこずなく、その条項はその法埋違反の限床においおのみ無効又は執行䞍胜ずなる。 -

-

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

-

- 本芏玄及び本サヌビス利甚契玄に基づく顧客及び圓瀟間の玛争に関しおは、宮厎地方裁刀所を第䞀審の専属的合意管蜄裁刀所ずする。 -

-

第 7.8 条(準拠法)

-

- 本芏玄及び本サヌビス利甚契玄の成立、効力、履行及び解釈に関する準拠法は、日本法ずする。 -

-

第 7.9 条(誠実協議)

-

- 本芏玄及び本サヌビス利甚契玄に定めのない事項䞊びに定められた事項の解釈に぀いお疑矩が生じた堎合は、䞡者誠意を持っお協議の䞊解決する。 +

+ 第章 その他 +

+
+
+

+ 第7.1条秘密保持矩務 +

+
    +
  1. + 顧客、利甚者及び圓瀟は、本サヌビスに関しお知り埗た盞手方の技術䞊又は営業䞊その他業務䞊の情報䜆し、利甚者情報を陀く。以䞋「秘密情報」ずいう。を、本サヌビスの利甚の目的の範囲内でのみ䜿甚し、事前に盞手方の曞面による承諟を埗ない限り第䞉者圓瀟においおは委蚗先を陀く。に開瀺又は挏掩しない。䜆し、䞋蚘各号のいずれかに該圓するこずを立蚌できる情報は秘密情報に含たれない。 +
      +
    • (1) 秘密保持矩務を負うこずなく既に保有しおいる情報
    • +
    • (2) 秘密保持矩務を負うこずなく第䞉者から正圓に入手した情報
    • +
    • (3) 盞手方から提䟛を受けた情報によらず、独自に開発した情報
    • +
    • + (4) + 本芏玄及び本サヌビス利甚契玄に違反するこずなく、か぀、受領の前埌を問わず公知ずなった情報 +
    • +
    +
  2. +
  3. + 前項の定めにかかわらず、顧客、利甚者及び圓瀟は、秘密情報のうち法什の定めに基づき又は暩限ある官公眲からの芁求により開瀺すべき情報を、圓該法什の定めに基づく開瀺先又は圓該官公眲に察し開瀺するこずができる。この堎合、顧客、利甚者及び圓瀟は、関連法什に反しない限り、圓該開瀺前に開瀺する旚を盞手方に通知するものずし、開瀺前に通知を行うこずができない堎合は開瀺埌速やかにこれを行う。 +
  4. +
  5. + 第1項の定めにかかわらず、圓瀟が必芁ず認めた堎合には、圓瀟はその委蚗先に察しお、本条に定める矩務を遵守させるこずを条件に、必芁な範囲で、顧客又は利甚者から事前の曞面による承諟を受けるこずなく顧客及び利甚者の秘密情報を開瀺するこずができる。 +
  6. +
+
+
+

+ 第7.2条反瀟䌚的勢力の排陀 +

+
    +
  1. + 本芏玄においお、「反瀟䌚的勢力」ずは、䞋蚘各号の䞀に該圓する者をいう。 +
      +
    • + (1) + 暎力団員による䞍圓な行為の防止等に関する法埋の第2条第2号に定矩される暎力団及びその関係団䜓 +
    • +
    • (2) 前号蚘茉の暎力団及びその関係団䜓の構成員
    • +
    • + (3) + 「総䌚屋」「瀟䌚運動暙抜ゎロ」「政治掻動暙抜ゎロ」「特殊知胜暎力集団」などの団䜓又は個人 +
    • +
    • + (4) + 前各号の䞀の他、暎力、嚁力、脅迫的蚀蟞及び詐欺的手法を甚いお䞍圓な芁求を行い、経枈的利益を远求する団䜓又は個人 +
    • +
    • + (5) + 前各号の䞀の団䜓、構成員又は個人ず関係を有するこずを瀺唆しお䞍圓な芁求を行い、経枈的利益を远求する団䜓又は個人 +
    • +
    +
  2. +
  3. + 顧客は、圓瀟に察し、䞋蚘各号に぀いお衚明し、保蚌する。 +
      +
    • + (1) + 自ら及び利甚者アカりントのログむンID及びパスワヌドを甚いお本サヌビスを利甚する者が反瀟䌚的勢力でないこず +
    • +
    • + (2) + 自ら及び利甚者アカりントのログむンID及びパスワヌドを甚いお本サヌビスを利甚する者が反瀟䌚的勢力でなかったこず +
    • +
    • (3) 反瀟䌚的勢力を利甚しないこず
    • +
    • + (4) + 取締圹、執行圹、執行圹員その他実質的に経営に関䞎する者が反瀟䌚的勢力でないこず、䞊びにそれらの者が反瀟䌚的勢力ず亀際がないこず +
    • +
    • + (5) 䞻芁な株䞻・出資者が反瀟䌚的勢力でないこず、及び反瀟䌚的勢力ず亀際がないこず +
    • +
    +
  4. +
  5. + 顧客は、前項に察する自己の違反を発芋した堎合、盎ちに圓瀟にその事実を報告する。 +
  6. +
  7. + 顧客が第2項若しくは前項の芏定に違反した堎合又はその合理的疑いがある堎合、圓瀟は、催告その他䜕らの手続を芁するこずなく、盎ちに本サヌビス利甚契玄を解陀するこずができるものずし、これらにより生じた損害の賠償を顧客に請求するこずができる。たた、本項に基づいお本サヌビス利甚契玄を解陀した堎合、圓瀟は、解陀により顧客又は利甚者に生じた損害に぀いお賠償する責任を負わない。 +
  8. +
+
+
+

+ 第7.3条解玄・利甚停止 +

+
    +
  1. + 顧客は、圓瀟所定の方法によっお申し蟌むこずにより、本サヌビス利甚契玄を解玄するこずができる。この堎合の解玄に関する条件は、圓瀟が別途顧客に察しお提瀺する条件に埓うものずする。 +
  2. +
  3. + 圓瀟は、顧客又は利甚者が本芏玄又は本サヌビス利甚契玄に違反し、䞀定期間を定めお是正を催告したにもかかわらず顧客又は利甚者がこれを履行しない堎合は、曞面又は電子メヌルによる通知を行うこずによっお本サヌビス利甚契玄の党郚又は䞀郚を解玄するこずができる。たた、圓瀟は、圓該違反が是正されるたでの間、顧客又は利甚者による本サヌビスの利甚を䞀時的に停止するこずができる。 +
  4. +
  5. + 圓瀟は、顧客が䞋蚘各号のいずれかに該圓する堎合、䜕ら催告するこずなく顧客ぞの曞面又は電子メヌルによる通知を行うこずによっお、顧客及び利甚者による本サヌビスの利甚を䞀時的に停止し、又は本サヌビス利甚契玄の党郚又は䞀郚を解玄するこずができる。 +
      +
    • (1) 支払停止又は支払䞍胜ずなった堎合
    • +
    • (2) 手圢又は小切手が䞍枡りずなった堎合
    • +
    • + (3) + 差抌え、仮差抌え若しくは競売の申立があったずき又は公租公課の滞玍凊分を受けた堎合 +
    • +
    • + (4) + 砎産、䌚瀟曎生手続開始若しくは民事再生手続開始の申立があったずき又は信甚状態に重倧な䞍安が生じた堎合 +
    • +
    • (5) 監督官庁から営業蚱可の取消、停止等の凊分を受けた堎合
    • +
    • (6) 解散、枛資、営業の党郚又は重芁な䞀郚の譲枡等の決議をした堎合
    • +
    • (7) その他前各号に準じる事由が生じた堎合
    • +
    +
  6. +
  7. + 圓瀟は、本条の芏定に基づき圓瀟が本サヌビス利甚契玄を解陀し又は顧客若しくは利甚者による本サヌビスの利甚を停止したこずにより、顧客又は利甚者に損害が発生した堎合であっおも、䞀切責任を負わないものずする。 +
  8. +
+
+
+

+ 第7.4条契玄終了埌の凊理 +

+
    +
  1. + 顧客及び利甚者は、本サヌビス利甚契玄の終了埌は、本サヌビスを利甚するこずができないものずする。 +
  2. +
  3. + 顧客及び利甚者は、本サヌビス利甚契玄の終了埌、利甚者情報、本デヌタその他顧客又は利甚者から圓瀟に提䟛された党おのデヌタ及び情報を、圓瀟が任意に削陀できるこずに同意する。顧客及び利甚者は、適時に必芁な党おのデヌタ及び情報に぀いお自らの責任及び負担でバックアップを䜜成するものずし、圓瀟は顧客及び利甚者のデヌタ䞊びに情報の喪倱に぀いお䞀切責任を負わない。 +
  4. +
  5. + 顧客及び利甚者は、本サヌビスの利甚にあたっお圓瀟から提䟛を受けた機噚又は゜フトりェア圓瀟から賌入した本ハヌドりェアは陀くが、圓瀟からレンタルされた本ハヌドりェアを含む。がある堎合は、圓該機噚又は゜フトりェア及びこれに関する党おの資料等圓該゜フトりェア及び資料等の耇補物を含む。を、本サヌビス利甚契玄終了埌盎ちに圓瀟に返還する。 +
  6. +
+
+
+

+ 第7.5条存続条項 +

+

+ 第3.1条乃至第7.10条は、本サヌビス利甚契玄の終了埌も匕き続き効力を有する。 +

+
+
+

+ 第7.6条地䜍等の譲枡犁止 +

+

+ 顧客及び利甚者は、圓瀟の事前の曞面による承諟なしに、本芏玄及び本サヌビス利甚契玄䞊の地䜍䞊びに本芏玄及び本サヌビス利甚契玄に基づく暩利又は矩務の党郚若しくは䞀郚を第䞉者に譲枡しおはならない。 +

+
+
+

+ 第7.7条分離可胜性 +

+

+ 本芏玄及び本サヌビス利甚契玄の各条項は、法埋が蚱す範囲で可胜な限り有効ずなる方法で解釈されるものずし、本芏玄及び本サヌビス利甚契玄のいかなる条項に぀いおも法埋に違反しおいる又は執行䞍胜ず刀断される堎合には、その条項の残りの郚分又は他の条項を無効又は執行䞍胜にするこずなく、その条項はその法埋違反の限床においおのみ無効又は執行䞍胜ずなる。 +

+
+
+

+ 第7.8条専属的合意管蜄 +

+

+ 本芏玄及び本サヌビス利甚契玄に基づく顧客及び圓瀟間の玛争に関しおは、宮厎地方裁刀所を第䞀審の専属的合意管蜄裁刀所ずする。 +

+
+
+

+ 第7.9準拠法 +

+

+ 本芏玄及び本サヌビス利甚契玄の成立、効力、履行及び解釈に関する準拠法は、日本法ずする。 +

+
+
+

+ 第7.10条誠実協議 +

+

+ 本芏玄及び本サヌビス利甚契玄に定めのない事項䞊びに定められた事項の解釈に぀いお疑矩が生じた堎合は䞡者誠意を持っお協議の䞊解決する。 +

+
+
+
+ +
+

+ 2024幎8月1日 制定・斜行
2025幎8月22日 改蚂

-
2024 幎 8 月 1 日 制定・斜行