diff --git a/desktop/src/generated/synkronus-client/.openapi-generator/FILES b/desktop/src/generated/synkronus-client/.openapi-generator/FILES index ca1252f4d..d61e94959 100644 --- a/desktop/src/generated/synkronus-client/.openapi-generator/FILES +++ b/desktop/src/generated/synkronus-client/.openapi-generator/FILES @@ -2,6 +2,7 @@ apis/AttachmentsApi.ts apis/DataExportApi.ts apis/DefaultApi.ts apis/HealthApi.ts +apis/StatsApi.ts apis/index.ts docs/APIVersionInfo.md docs/APIVersionsResponse.md @@ -34,7 +35,11 @@ docs/GetHealth503Response.md docs/HealthApi.md docs/LoginRequest.md docs/Observation.md +docs/ObservationFormTypeCount.md docs/ObservationGeolocation.md +docs/ObservationStatsResponse.md +docs/ObservationTimeline.md +docs/ObservationTimelineBucket.md docs/ProblemDetail.md docs/ProblemDetailErrorsInner.md docs/RefreshTokenRequest.md @@ -43,6 +48,7 @@ docs/RepositoryResetResponse.md docs/ResetUserPassword200Response.md docs/ResetUserPasswordRequest.md docs/ServerInfo.md +docs/StatsApi.md docs/SwitchAppBundleVersion200Response.md docs/SyncPullRequest.md docs/SyncPullRequestSince.md @@ -85,7 +91,11 @@ models/GetHealth200Response.ts models/GetHealth503Response.ts models/LoginRequest.ts models/Observation.ts +models/ObservationFormTypeCount.ts models/ObservationGeolocation.ts +models/ObservationStatsResponse.ts +models/ObservationTimeline.ts +models/ObservationTimelineBucket.ts models/ProblemDetail.ts models/ProblemDetailErrorsInner.ts models/RefreshTokenRequest.ts diff --git a/desktop/src/generated/synkronus-client/apis/StatsApi.ts b/desktop/src/generated/synkronus-client/apis/StatsApi.ts new file mode 100644 index 000000000..5480c7d7a --- /dev/null +++ b/desktop/src/generated/synkronus-client/apis/StatsApi.ts @@ -0,0 +1,90 @@ +// @ts-nocheck +/* tslint:disable */ +/* eslint-disable */ +/** + * Synkronus API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.6 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import * as runtime from '../runtime'; +import type { + ErrorResponse, + ObservationStatsResponse, +} from '../models/index'; + +export interface GetObservationStatsRequest { + xOdeVersion: string; +} + +/** + * + */ +export class StatsApi extends runtime.BaseAPI { + + /** + * Creates request options for getObservationStats without sending the request + */ + async getObservationStatsRequestOpts(requestParameters: GetObservationStatsRequest): Promise { + if (requestParameters['xOdeVersion'] == null) { + throw new runtime.RequiredError( + 'xOdeVersion', + 'Required parameter "xOdeVersion" was null or undefined when calling getObservationStats().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (requestParameters['xOdeVersion'] != null) { + headerParameters['x-ode-version'] = String(requestParameters['xOdeVersion']); + } + + if (this.configuration && this.configuration.accessToken) { + const token = this.configuration.accessToken; + const tokenString = await token("bearerAuth", ["read-only", "read-write", "admin"]); + + if (tokenString) { + headerParameters["Authorization"] = `Bearer ${tokenString}`; + } + } + + let urlPath = `/api/stats/observations`; + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + * Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + * Observation aggregate stats for dashboard charts + */ + async getObservationStatsRaw(requestParameters: GetObservationStatsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.getObservationStatsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response); + } + + /** + * Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + * Observation aggregate stats for dashboard charts + */ + async getObservationStats(requestParameters: GetObservationStatsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.getObservationStatsRaw(requestParameters, initOverrides); + return await response.value(); + } + +} diff --git a/desktop/src/generated/synkronus-client/apis/index.ts b/desktop/src/generated/synkronus-client/apis/index.ts index e234afe0f..32a8995a4 100644 --- a/desktop/src/generated/synkronus-client/apis/index.ts +++ b/desktop/src/generated/synkronus-client/apis/index.ts @@ -5,3 +5,4 @@ export * from './AttachmentsApi'; export * from './DataExportApi'; export * from './DefaultApi'; export * from './HealthApi'; +export * from './StatsApi'; diff --git a/desktop/src/generated/synkronus-client/docs/ObservationFormTypeCount.md b/desktop/src/generated/synkronus-client/docs/ObservationFormTypeCount.md new file mode 100644 index 000000000..85507dfe9 --- /dev/null +++ b/desktop/src/generated/synkronus-client/docs/ObservationFormTypeCount.md @@ -0,0 +1,15 @@ + +# ObservationFormTypeCount + + +## Properties + +Name | Type +------------ | ------------- +`formType` | string +`count` | number + + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/desktop/src/generated/synkronus-client/docs/ObservationStatsResponse.md b/desktop/src/generated/synkronus-client/docs/ObservationStatsResponse.md new file mode 100644 index 000000000..c4d84d1a4 --- /dev/null +++ b/desktop/src/generated/synkronus-client/docs/ObservationStatsResponse.md @@ -0,0 +1,17 @@ + +# ObservationStatsResponse + + +## Properties + +Name | Type +------------ | ------------- +`totalCount` | number +`byFormType` | [Array<ObservationFormTypeCount>](ObservationFormTypeCount.md) +`timeline` | [ObservationTimeline](ObservationTimeline.md) +`computedAt` | Date + + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/desktop/src/generated/synkronus-client/docs/ObservationTimeline.md b/desktop/src/generated/synkronus-client/docs/ObservationTimeline.md new file mode 100644 index 000000000..fa67f51a2 --- /dev/null +++ b/desktop/src/generated/synkronus-client/docs/ObservationTimeline.md @@ -0,0 +1,17 @@ + +# ObservationTimeline + + +## Properties + +Name | Type +------------ | ------------- +`bucketUnit` | string +`rangeStart` | string +`rangeEnd` | string +`buckets` | [Array<ObservationTimelineBucket>](ObservationTimelineBucket.md) + + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/desktop/src/generated/synkronus-client/docs/ObservationTimelineBucket.md b/desktop/src/generated/synkronus-client/docs/ObservationTimelineBucket.md new file mode 100644 index 000000000..1dfdb7244 --- /dev/null +++ b/desktop/src/generated/synkronus-client/docs/ObservationTimelineBucket.md @@ -0,0 +1,16 @@ + +# ObservationTimelineBucket + + +## Properties + +Name | Type +------------ | ------------- +`bucketStart` | string +`label` | string +`count` | number + + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/desktop/src/generated/synkronus-client/docs/StatsApi.md b/desktop/src/generated/synkronus-client/docs/StatsApi.md new file mode 100644 index 000000000..aa9c0f82c --- /dev/null +++ b/desktop/src/generated/synkronus-client/docs/StatsApi.md @@ -0,0 +1,83 @@ +# StatsApi + +All URIs are relative to *http://localhost* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**getObservationStats**](StatsApi.md#getobservationstats) | **GET** /api/stats/observations | Observation aggregate stats for dashboard charts | + + + +## getObservationStats + +> ObservationStatsResponse getObservationStats(xOdeVersion) + +Observation aggregate stats for dashboard charts + +Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + +### Example + +```ts +import { + Configuration, + StatsApi, +} from ''; +import type { GetObservationStatsRequest } from ''; + +async function example() { + console.log("πŸš€ Testing SDK..."); + const config = new Configuration({ + // Configure HTTP bearer authorization: bearerAuth + accessToken: "YOUR BEARER TOKEN", + }); + const api = new StatsApi(config); + + const body = { + // string | Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). + xOdeVersion: 1.0.0, + } satisfies GetObservationStatsRequest; + + try { + const data = await api.getObservationStats(body); + console.log(data); + } catch (error) { + console.error(error); + } +} + +// Run the test +example().catch(console.error); +``` + +### Parameters + + +| Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **xOdeVersion** | `string` | Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). | [Defaults to `undefined`] | + +### Return type + +[**ObservationStatsResponse**](ObservationStatsResponse.md) + +### Authorization + +[bearerAuth](../README.md#bearerAuth) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: `application/json` + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | Observation aggregate stats | - | +| **401** | Unauthorized | - | +| **403** | Forbidden | - | +| **500** | Internal server error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + diff --git a/desktop/src/generated/synkronus-client/models/ObservationFormTypeCount.ts b/desktop/src/generated/synkronus-client/models/ObservationFormTypeCount.ts new file mode 100644 index 000000000..0ba36144f --- /dev/null +++ b/desktop/src/generated/synkronus-client/models/ObservationFormTypeCount.ts @@ -0,0 +1,76 @@ +// @ts-nocheck +/* tslint:disable */ +/* eslint-disable */ +/** + * Synkronus API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.6 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ObservationFormTypeCount + */ +export interface ObservationFormTypeCount { + /** + * Form type id; empty/whitespace stored values become "(no form type)" + * @type {string} + * @memberof ObservationFormTypeCount + */ + formType: string; + /** + * + * @type {number} + * @memberof ObservationFormTypeCount + */ + count: number; +} + +/** + * Check if a given object implements the ObservationFormTypeCount interface. + */ +export function instanceOfObservationFormTypeCount(value: object): value is ObservationFormTypeCount { + if (!('formType' in value) || value['formType'] === undefined) return false; + if (!('count' in value) || value['count'] === undefined) return false; + return true; +} + +export function ObservationFormTypeCountFromJSON(json: any): ObservationFormTypeCount { + return ObservationFormTypeCountFromJSONTyped(json, false); +} + +export function ObservationFormTypeCountFromJSONTyped(json: any, ignoreDiscriminator: boolean): ObservationFormTypeCount { + if (json == null) { + return json; + } + return { + + 'formType': json['formType'], + 'count': json['count'], + }; +} + +export function ObservationFormTypeCountToJSON(json: any): ObservationFormTypeCount { + return ObservationFormTypeCountToJSONTyped(json, false); +} + +export function ObservationFormTypeCountToJSONTyped(value?: ObservationFormTypeCount | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'formType': value['formType'], + 'count': value['count'], + }; +} + diff --git a/desktop/src/generated/synkronus-client/models/ObservationStatsResponse.ts b/desktop/src/generated/synkronus-client/models/ObservationStatsResponse.ts new file mode 100644 index 000000000..83448d6ba --- /dev/null +++ b/desktop/src/generated/synkronus-client/models/ObservationStatsResponse.ts @@ -0,0 +1,109 @@ +// @ts-nocheck +/* tslint:disable */ +/* eslint-disable */ +/** + * Synkronus API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.6 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { ObservationTimeline } from './ObservationTimeline'; +import { + ObservationTimelineFromJSON, + ObservationTimelineFromJSONTyped, + ObservationTimelineToJSON, + ObservationTimelineToJSONTyped, +} from './ObservationTimeline'; +import type { ObservationFormTypeCount } from './ObservationFormTypeCount'; +import { + ObservationFormTypeCountFromJSON, + ObservationFormTypeCountFromJSONTyped, + ObservationFormTypeCountToJSON, + ObservationFormTypeCountToJSONTyped, +} from './ObservationFormTypeCount'; + +/** + * + * @export + * @interface ObservationStatsResponse + */ +export interface ObservationStatsResponse { + /** + * Total non-deleted observations + * @type {number} + * @memberof ObservationStatsResponse + */ + totalCount: number; + /** + * + * @type {Array} + * @memberof ObservationStatsResponse + */ + byFormType: Array; + /** + * + * @type {ObservationTimeline} + * @memberof ObservationStatsResponse + */ + timeline: ObservationTimeline; + /** + * UTC timestamp when this aggregate was computed + * @type {Date} + * @memberof ObservationStatsResponse + */ + computedAt: Date; +} + +/** + * Check if a given object implements the ObservationStatsResponse interface. + */ +export function instanceOfObservationStatsResponse(value: object): value is ObservationStatsResponse { + if (!('totalCount' in value) || value['totalCount'] === undefined) return false; + if (!('byFormType' in value) || value['byFormType'] === undefined) return false; + if (!('timeline' in value) || value['timeline'] === undefined) return false; + if (!('computedAt' in value) || value['computedAt'] === undefined) return false; + return true; +} + +export function ObservationStatsResponseFromJSON(json: any): ObservationStatsResponse { + return ObservationStatsResponseFromJSONTyped(json, false); +} + +export function ObservationStatsResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): ObservationStatsResponse { + if (json == null) { + return json; + } + return { + + 'totalCount': json['totalCount'], + 'byFormType': ((json['byFormType'] as Array).map(ObservationFormTypeCountFromJSON)), + 'timeline': ObservationTimelineFromJSON(json['timeline']), + 'computedAt': (new Date(json['computedAt'])), + }; +} + +export function ObservationStatsResponseToJSON(json: any): ObservationStatsResponse { + return ObservationStatsResponseToJSONTyped(json, false); +} + +export function ObservationStatsResponseToJSONTyped(value?: ObservationStatsResponse | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'totalCount': value['totalCount'], + 'byFormType': ((value['byFormType'] as Array).map(ObservationFormTypeCountToJSON)), + 'timeline': ObservationTimelineToJSON(value['timeline']), + 'computedAt': value['computedAt'].toISOString(), + }; +} + diff --git a/desktop/src/generated/synkronus-client/models/ObservationTimeline.ts b/desktop/src/generated/synkronus-client/models/ObservationTimeline.ts new file mode 100644 index 000000000..78f3c00bc --- /dev/null +++ b/desktop/src/generated/synkronus-client/models/ObservationTimeline.ts @@ -0,0 +1,113 @@ +// @ts-nocheck +/* tslint:disable */ +/* eslint-disable */ +/** + * Synkronus API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.6 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { ObservationTimelineBucket } from './ObservationTimelineBucket'; +import { + ObservationTimelineBucketFromJSON, + ObservationTimelineBucketFromJSONTyped, + ObservationTimelineBucketToJSON, + ObservationTimelineBucketToJSONTyped, +} from './ObservationTimelineBucket'; + +/** + * + * @export + * @interface ObservationTimeline + */ +export interface ObservationTimeline { + /** + * Bucket granularity (week when span of dated observations is >= 365 days) + * @type {ObservationTimelineBucketUnitEnum} + * @memberof ObservationTimeline + */ + bucketUnit: ObservationTimelineBucketUnitEnum; + /** + * First bucket start date (YYYY-MM-DD), empty when there are no observations + * @type {string} + * @memberof ObservationTimeline + */ + rangeStart: string; + /** + * Last bucket start date (YYYY-MM-DD), empty when there are no observations + * @type {string} + * @memberof ObservationTimeline + */ + rangeEnd: string; + /** + * Dense zero-filled buckets from rangeStart through rangeEnd + * @type {Array} + * @memberof ObservationTimeline + */ + buckets: Array; +} + + +/** + * @export + */ +export const ObservationTimelineBucketUnitEnum = { + Day: 'day', + Week: 'week' +} as const; +export type ObservationTimelineBucketUnitEnum = typeof ObservationTimelineBucketUnitEnum[keyof typeof ObservationTimelineBucketUnitEnum]; + + +/** + * Check if a given object implements the ObservationTimeline interface. + */ +export function instanceOfObservationTimeline(value: object): value is ObservationTimeline { + if (!('bucketUnit' in value) || value['bucketUnit'] === undefined) return false; + if (!('rangeStart' in value) || value['rangeStart'] === undefined) return false; + if (!('rangeEnd' in value) || value['rangeEnd'] === undefined) return false; + if (!('buckets' in value) || value['buckets'] === undefined) return false; + return true; +} + +export function ObservationTimelineFromJSON(json: any): ObservationTimeline { + return ObservationTimelineFromJSONTyped(json, false); +} + +export function ObservationTimelineFromJSONTyped(json: any, ignoreDiscriminator: boolean): ObservationTimeline { + if (json == null) { + return json; + } + return { + + 'bucketUnit': json['bucketUnit'], + 'rangeStart': json['rangeStart'], + 'rangeEnd': json['rangeEnd'], + 'buckets': ((json['buckets'] as Array).map(ObservationTimelineBucketFromJSON)), + }; +} + +export function ObservationTimelineToJSON(json: any): ObservationTimeline { + return ObservationTimelineToJSONTyped(json, false); +} + +export function ObservationTimelineToJSONTyped(value?: ObservationTimeline | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'bucketUnit': value['bucketUnit'], + 'rangeStart': value['rangeStart'], + 'rangeEnd': value['rangeEnd'], + 'buckets': ((value['buckets'] as Array).map(ObservationTimelineBucketToJSON)), + }; +} + diff --git a/desktop/src/generated/synkronus-client/models/ObservationTimelineBucket.ts b/desktop/src/generated/synkronus-client/models/ObservationTimelineBucket.ts new file mode 100644 index 000000000..6396adbaf --- /dev/null +++ b/desktop/src/generated/synkronus-client/models/ObservationTimelineBucket.ts @@ -0,0 +1,85 @@ +// @ts-nocheck +/* tslint:disable */ +/* eslint-disable */ +/** + * Synkronus API + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 1.0.6 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface ObservationTimelineBucket + */ +export interface ObservationTimelineBucket { + /** + * Bucket start date (YYYY-MM-DD, UTC) + * @type {string} + * @memberof ObservationTimelineBucket + */ + bucketStart: string; + /** + * Human-readable label (e.g. "Jan 1") + * @type {string} + * @memberof ObservationTimelineBucket + */ + label: string; + /** + * + * @type {number} + * @memberof ObservationTimelineBucket + */ + count: number; +} + +/** + * Check if a given object implements the ObservationTimelineBucket interface. + */ +export function instanceOfObservationTimelineBucket(value: object): value is ObservationTimelineBucket { + if (!('bucketStart' in value) || value['bucketStart'] === undefined) return false; + if (!('label' in value) || value['label'] === undefined) return false; + if (!('count' in value) || value['count'] === undefined) return false; + return true; +} + +export function ObservationTimelineBucketFromJSON(json: any): ObservationTimelineBucket { + return ObservationTimelineBucketFromJSONTyped(json, false); +} + +export function ObservationTimelineBucketFromJSONTyped(json: any, ignoreDiscriminator: boolean): ObservationTimelineBucket { + if (json == null) { + return json; + } + return { + + 'bucketStart': json['bucketStart'], + 'label': json['label'], + 'count': json['count'], + }; +} + +export function ObservationTimelineBucketToJSON(json: any): ObservationTimelineBucket { + return ObservationTimelineBucketToJSONTyped(json, false); +} + +export function ObservationTimelineBucketToJSONTyped(value?: ObservationTimelineBucket | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'bucketStart': value['bucketStart'], + 'label': value['label'], + 'count': value['count'], + }; +} + diff --git a/desktop/src/generated/synkronus-client/models/index.ts b/desktop/src/generated/synkronus-client/models/index.ts index 6396a4a34..87546305f 100644 --- a/desktop/src/generated/synkronus-client/models/index.ts +++ b/desktop/src/generated/synkronus-client/models/index.ts @@ -803,6 +803,25 @@ export interface Observation { */ tags?: Array | null; } +/** + * + * @export + * @interface ObservationFormTypeCount + */ +export interface ObservationFormTypeCount { + /** + * Form type id; empty/whitespace stored values become "(no form type)" + * @type {string} + * @memberof ObservationFormTypeCount + */ + formType: string; + /** + * + * @type {number} + * @memberof ObservationFormTypeCount + */ + count: number; +} /** * Optional geolocation data for the observation * @export @@ -846,6 +865,104 @@ export interface ObservationGeolocation { */ timestamp?: Date; } +/** + * + * @export + * @interface ObservationStatsResponse + */ +export interface ObservationStatsResponse { + /** + * Total non-deleted observations + * @type {number} + * @memberof ObservationStatsResponse + */ + totalCount: number; + /** + * + * @type {Array} + * @memberof ObservationStatsResponse + */ + byFormType: Array; + /** + * + * @type {ObservationTimeline} + * @memberof ObservationStatsResponse + */ + timeline: ObservationTimeline; + /** + * UTC timestamp when this aggregate was computed + * @type {Date} + * @memberof ObservationStatsResponse + */ + computedAt: Date; +} +/** + * + * @export + * @interface ObservationTimeline + */ +export interface ObservationTimeline { + /** + * Bucket granularity (week when span of dated observations is >= 365 days) + * @type {ObservationTimelineBucketUnitEnum} + * @memberof ObservationTimeline + */ + bucketUnit: ObservationTimelineBucketUnitEnum; + /** + * First bucket start date (YYYY-MM-DD), empty when there are no observations + * @type {string} + * @memberof ObservationTimeline + */ + rangeStart: string; + /** + * Last bucket start date (YYYY-MM-DD), empty when there are no observations + * @type {string} + * @memberof ObservationTimeline + */ + rangeEnd: string; + /** + * Dense zero-filled buckets from rangeStart through rangeEnd + * @type {Array} + * @memberof ObservationTimeline + */ + buckets: Array; +} + + +/** + * @export + */ +export const ObservationTimelineBucketUnitEnum = { + Day: 'day', + Week: 'week' +} as const; +export type ObservationTimelineBucketUnitEnum = typeof ObservationTimelineBucketUnitEnum[keyof typeof ObservationTimelineBucketUnitEnum]; + +/** + * + * @export + * @interface ObservationTimelineBucket + */ +export interface ObservationTimelineBucket { + /** + * Bucket start date (YYYY-MM-DD, UTC) + * @type {string} + * @memberof ObservationTimelineBucket + */ + bucketStart: string; + /** + * Human-readable label (e.g. "Jan 1") + * @type {string} + * @memberof ObservationTimelineBucket + */ + label: string; + /** + * + * @type {number} + * @memberof ObservationTimelineBucket + */ + count: number; +} /** * * @export diff --git a/formulus/src/api/synkronus/generated/.openapi-generator/FILES b/formulus/src/api/synkronus/generated/.openapi-generator/FILES index 03184907e..a9761a534 100644 --- a/formulus/src/api/synkronus/generated/.openapi-generator/FILES +++ b/formulus/src/api/synkronus/generated/.openapi-generator/FILES @@ -35,7 +35,11 @@ docs/GetHealth503Response.md docs/HealthApi.md docs/LoginRequest.md docs/Observation.md +docs/ObservationFormTypeCount.md docs/ObservationGeolocation.md +docs/ObservationStatsResponse.md +docs/ObservationTimeline.md +docs/ObservationTimelineBucket.md docs/ProblemDetail.md docs/ProblemDetailErrorsInner.md docs/RefreshTokenRequest.md @@ -44,6 +48,7 @@ docs/RepositoryResetResponse.md docs/ResetUserPassword200Response.md docs/ResetUserPasswordRequest.md docs/ServerInfo.md +docs/StatsApi.md docs/SwitchAppBundleVersion200Response.md docs/SyncPullRequest.md docs/SyncPullRequestSince.md diff --git a/formulus/src/api/synkronus/generated/api.ts b/formulus/src/api/synkronus/generated/api.ts index fb84d0d88..dd55b6545 100644 --- a/formulus/src/api/synkronus/generated/api.ts +++ b/formulus/src/api/synkronus/generated/api.ts @@ -833,6 +833,25 @@ export interface Observation { */ tags?: Array | null; } +/** + * + * @export + * @interface ObservationFormTypeCount + */ +export interface ObservationFormTypeCount { + /** + * Form type id; empty/whitespace stored values become \"(no form type)\" + * @type {string} + * @memberof ObservationFormTypeCount + */ + formType: string; + /** + * + * @type {number} + * @memberof ObservationFormTypeCount + */ + count: number; +} /** * Optional geolocation data for the observation * @export @@ -876,6 +895,102 @@ export interface ObservationGeolocation { */ timestamp?: string; } +/** + * + * @export + * @interface ObservationStatsResponse + */ +export interface ObservationStatsResponse { + /** + * Total non-deleted observations + * @type {number} + * @memberof ObservationStatsResponse + */ + totalCount: number; + /** + * + * @type {Array} + * @memberof ObservationStatsResponse + */ + byFormType: Array; + /** + * + * @type {ObservationTimeline} + * @memberof ObservationStatsResponse + */ + timeline: ObservationTimeline; + /** + * UTC timestamp when this aggregate was computed + * @type {string} + * @memberof ObservationStatsResponse + */ + computedAt: string; +} +/** + * + * @export + * @interface ObservationTimeline + */ +export interface ObservationTimeline { + /** + * Bucket granularity (week when span of dated observations is >= 365 days) + * @type {string} + * @memberof ObservationTimeline + */ + bucketUnit: ObservationTimelineBucketUnitEnum; + /** + * First bucket start date (YYYY-MM-DD), empty when there are no observations + * @type {string} + * @memberof ObservationTimeline + */ + rangeStart: string; + /** + * Last bucket start date (YYYY-MM-DD), empty when there are no observations + * @type {string} + * @memberof ObservationTimeline + */ + rangeEnd: string; + /** + * Dense zero-filled buckets from rangeStart through rangeEnd + * @type {Array} + * @memberof ObservationTimeline + */ + buckets: Array; +} + +export const ObservationTimelineBucketUnitEnum = { + Day: 'day', + Week: 'week', +} as const; + +export type ObservationTimelineBucketUnitEnum = + (typeof ObservationTimelineBucketUnitEnum)[keyof typeof ObservationTimelineBucketUnitEnum]; + +/** + * + * @export + * @interface ObservationTimelineBucket + */ +export interface ObservationTimelineBucket { + /** + * Bucket start date (YYYY-MM-DD, UTC) + * @type {string} + * @memberof ObservationTimelineBucket + */ + bucketStart: string; + /** + * Human-readable label (e.g. \"Jan 1\") + * @type {string} + * @memberof ObservationTimelineBucket + */ + label: string; + /** + * + * @type {number} + * @memberof ObservationTimelineBucket + */ + count: number; +} /** * * @export @@ -5846,3 +5961,176 @@ export class HealthApi extends BaseAPI { .then(request => request(this.axios, this.basePath)); } } + +/** + * StatsApi - axios parameter creator + * @export + */ +export const StatsApiAxiosParamCreator = function ( + configuration?: Configuration, +) { + return { + /** + * Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + * @summary Observation aggregate stats for dashboard charts + * @param {string} xOdeVersion Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getObservationStats: async ( + xOdeVersion: string, + options: RawAxiosRequestConfig = {}, + ): Promise => { + // verify required parameter 'xOdeVersion' is not null or undefined + assertParamExists('getObservationStats', 'xOdeVersion', xOdeVersion); + const localVarPath = `/api/stats/observations`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { + method: 'GET', + ...baseOptions, + ...options, + }; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration); + + if (xOdeVersion != null) { + localVarHeaderParameter['x-ode-version'] = String(xOdeVersion); + } + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = + baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = { + ...localVarHeaderParameter, + ...headersFromBaseOptions, + ...options.headers, + }; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + }; +}; + +/** + * StatsApi - functional programming interface + * @export + */ +export const StatsApiFp = function (configuration?: Configuration) { + const localVarAxiosParamCreator = StatsApiAxiosParamCreator(configuration); + return { + /** + * Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + * @summary Observation aggregate stats for dashboard charts + * @param {string} xOdeVersion Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getObservationStats( + xOdeVersion: string, + options?: RawAxiosRequestConfig, + ): Promise< + ( + axios?: AxiosInstance, + basePath?: string, + ) => AxiosPromise + > { + const localVarAxiosArgs = + await localVarAxiosParamCreator.getObservationStats( + xOdeVersion, + options, + ); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = + operationServerMap['StatsApi.getObservationStats']?.[ + localVarOperationServerIndex + ]?.url; + return (axios, basePath) => + createRequestFunction( + localVarAxiosArgs, + globalAxios, + BASE_PATH, + configuration, + )(axios, localVarOperationServerBasePath || basePath); + }, + }; +}; + +/** + * StatsApi - factory interface + * @export + */ +export const StatsApiFactory = function ( + configuration?: Configuration, + basePath?: string, + axios?: AxiosInstance, +) { + const localVarFp = StatsApiFp(configuration); + return { + /** + * Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + * @summary Observation aggregate stats for dashboard charts + * @param {StatsApiGetObservationStatsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getObservationStats( + requestParameters: StatsApiGetObservationStatsRequest, + options?: RawAxiosRequestConfig, + ): AxiosPromise { + return localVarFp + .getObservationStats(requestParameters.xOdeVersion, options) + .then(request => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for getObservationStats operation in StatsApi. + * @export + * @interface StatsApiGetObservationStatsRequest + */ +export interface StatsApiGetObservationStatsRequest { + /** + * Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). + * @type {string} + * @memberof StatsApiGetObservationStats + */ + readonly xOdeVersion: string; +} + +/** + * StatsApi - object-oriented interface + * @export + * @class StatsApi + * @extends {BaseAPI} + */ +export class StatsApi extends BaseAPI { + /** + * Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + * @summary Observation aggregate stats for dashboard charts + * @param {StatsApiGetObservationStatsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof StatsApi + */ + public getObservationStats( + requestParameters: StatsApiGetObservationStatsRequest, + options?: RawAxiosRequestConfig, + ) { + return StatsApiFp(this.configuration) + .getObservationStats(requestParameters.xOdeVersion, options) + .then(request => request(this.axios, this.basePath)); + } +} diff --git a/formulus/src/api/synkronus/generated/docs/ObservationFormTypeCount.md b/formulus/src/api/synkronus/generated/docs/ObservationFormTypeCount.md new file mode 100644 index 000000000..2ba2a6c54 --- /dev/null +++ b/formulus/src/api/synkronus/generated/docs/ObservationFormTypeCount.md @@ -0,0 +1,21 @@ +# ObservationFormTypeCount + +## Properties + +| Name | Type | Description | Notes | +| ------------ | ---------- | -------------------------------------------------------------------------------- | ---------------------- | +| **formType** | **string** | Form type id; empty/whitespace stored values become \"(no form type)\" | [default to undefined] | +| **count** | **number** | | [default to undefined] | + +## Example + +```typescript +import { ObservationFormTypeCount } from './api'; + +const instance: ObservationFormTypeCount = { + formType, + count, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/formulus/src/api/synkronus/generated/docs/ObservationStatsResponse.md b/formulus/src/api/synkronus/generated/docs/ObservationStatsResponse.md new file mode 100644 index 000000000..a69eee543 --- /dev/null +++ b/formulus/src/api/synkronus/generated/docs/ObservationStatsResponse.md @@ -0,0 +1,25 @@ +# ObservationStatsResponse + +## Properties + +| Name | Type | Description | Notes | +| -------------- | ------------------------------------------------------------------------ | ---------------------------------------------- | ---------------------- | +| **totalCount** | **number** | Total non-deleted observations | [default to undefined] | +| **byFormType** | [**Array<ObservationFormTypeCount>**](ObservationFormTypeCount.md) | | [default to undefined] | +| **timeline** | [**ObservationTimeline**](ObservationTimeline.md) | | [default to undefined] | +| **computedAt** | **string** | UTC timestamp when this aggregate was computed | [default to undefined] | + +## Example + +```typescript +import { ObservationStatsResponse } from './api'; + +const instance: ObservationStatsResponse = { + totalCount, + byFormType, + timeline, + computedAt, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/formulus/src/api/synkronus/generated/docs/ObservationTimeline.md b/formulus/src/api/synkronus/generated/docs/ObservationTimeline.md new file mode 100644 index 000000000..db595be36 --- /dev/null +++ b/formulus/src/api/synkronus/generated/docs/ObservationTimeline.md @@ -0,0 +1,25 @@ +# ObservationTimeline + +## Properties + +| Name | Type | Description | Notes | +| -------------- | -------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ---------------------- | +| **bucketUnit** | **string** | Bucket granularity (week when span of dated observations is >= 365 days) | [default to undefined] | +| **rangeStart** | **string** | First bucket start date (YYYY-MM-DD), empty when there are no observations | [default to undefined] | +| **rangeEnd** | **string** | Last bucket start date (YYYY-MM-DD), empty when there are no observations | [default to undefined] | +| **buckets** | [**Array<ObservationTimelineBucket>**](ObservationTimelineBucket.md) | Dense zero-filled buckets from rangeStart through rangeEnd | [default to undefined] | + +## Example + +```typescript +import { ObservationTimeline } from './api'; + +const instance: ObservationTimeline = { + bucketUnit, + rangeStart, + rangeEnd, + buckets, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/formulus/src/api/synkronus/generated/docs/ObservationTimelineBucket.md b/formulus/src/api/synkronus/generated/docs/ObservationTimelineBucket.md new file mode 100644 index 000000000..33fcbe845 --- /dev/null +++ b/formulus/src/api/synkronus/generated/docs/ObservationTimelineBucket.md @@ -0,0 +1,23 @@ +# ObservationTimelineBucket + +## Properties + +| Name | Type | Description | Notes | +| --------------- | ---------- | ----------------------------------------------- | ---------------------- | +| **bucketStart** | **string** | Bucket start date (YYYY-MM-DD, UTC) | [default to undefined] | +| **label** | **string** | Human-readable label (e.g. \"Jan 1\") | [default to undefined] | +| **count** | **number** | | [default to undefined] | + +## Example + +```typescript +import { ObservationTimelineBucket } from './api'; + +const instance: ObservationTimelineBucket = { + bucketStart, + label, + count, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/formulus/src/api/synkronus/generated/docs/StatsApi.md b/formulus/src/api/synkronus/generated/docs/StatsApi.md new file mode 100644 index 000000000..0f0efbfc9 --- /dev/null +++ b/formulus/src/api/synkronus/generated/docs/StatsApi.md @@ -0,0 +1,56 @@ +# StatsApi + +All URIs are relative to _http://localhost_ + +| Method | HTTP request | Description | +| ----------------------------------------------- | ------------------------------- | ------------------------------------------------ | +| [**getObservationStats**](#getobservationstats) | **GET** /api/stats/observations | Observation aggregate stats for dashboard charts | + +# **getObservationStats** + +> ObservationStatsResponse getObservationStats() + +Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + +### Example + +```typescript +import { StatsApi, Configuration } from './api'; + +const configuration = new Configuration(); +const apiInstance = new StatsApi(configuration); + +let xOdeVersion: string; //Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). (default to undefined) + +const { status, data } = await apiInstance.getObservationStats(xOdeVersion); +``` + +### Parameters + +| Name | Type | Description | Notes | +| --------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| **xOdeVersion** | [**string**] | Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). | defaults to undefined | + +### Return type + +**ObservationStatsResponse** + +### Authorization + +[bearerAuth](../README.md#bearerAuth) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +### HTTP response details + +| Status code | Description | Response headers | +| ----------- | --------------------------- | ---------------- | +| **200** | Observation aggregate stats | - | +| **401** | Unauthorized | - | +| **403** | Forbidden | - | +| **500** | Internal server error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) diff --git a/synkronus-cli/pkg/client/generated/client.gen.go b/synkronus-cli/pkg/client/generated/client.gen.go index 2e543eaf6..925f7d306 100644 --- a/synkronus-cli/pkg/client/generated/client.gen.go +++ b/synkronus-cli/pkg/client/generated/client.gen.go @@ -40,6 +40,24 @@ func (e AttachmentOperationOperation) Valid() bool { } } +// Defines values for ObservationTimelineBucketUnit. +const ( + Day ObservationTimelineBucketUnit = "day" + Week ObservationTimelineBucketUnit = "week" +) + +// Valid indicates whether the value is a known member of the ObservationTimelineBucketUnit enum. +func (e ObservationTimelineBucketUnit) Valid() bool { + switch e { + case Day: + return true + case Week: + return true + default: + return false + } +} + // Defines values for RepositoryResetRequestConfirm. const ( RESETREPOSITORY RepositoryResetRequestConfirm = "RESET_REPOSITORY" @@ -334,6 +352,54 @@ type Observation struct { UpdatedAt time.Time `json:"updated_at"` } +// ObservationFormTypeCount defines model for ObservationFormTypeCount. +type ObservationFormTypeCount struct { + Count int64 `json:"count"` + + // FormType Form type id; empty/whitespace stored values become "(no form type)" + FormType string `json:"formType"` +} + +// ObservationStatsResponse defines model for ObservationStatsResponse. +type ObservationStatsResponse struct { + ByFormType []ObservationFormTypeCount `json:"byFormType"` + + // ComputedAt UTC timestamp when this aggregate was computed + ComputedAt time.Time `json:"computedAt"` + Timeline ObservationTimeline `json:"timeline"` + + // TotalCount Total non-deleted observations + TotalCount int64 `json:"totalCount"` +} + +// ObservationTimeline defines model for ObservationTimeline. +type ObservationTimeline struct { + // BucketUnit Bucket granularity (week when span of dated observations is >= 365 days) + BucketUnit ObservationTimelineBucketUnit `json:"bucketUnit"` + + // Buckets Dense zero-filled buckets from rangeStart through rangeEnd + Buckets []ObservationTimelineBucket `json:"buckets"` + + // RangeEnd Last bucket start date (YYYY-MM-DD), empty when there are no observations + RangeEnd string `json:"rangeEnd"` + + // RangeStart First bucket start date (YYYY-MM-DD), empty when there are no observations + RangeStart string `json:"rangeStart"` +} + +// ObservationTimelineBucketUnit Bucket granularity (week when span of dated observations is >= 365 days) +type ObservationTimelineBucketUnit string + +// ObservationTimelineBucket defines model for ObservationTimelineBucket. +type ObservationTimelineBucket struct { + // BucketStart Bucket start date (YYYY-MM-DD, UTC) + BucketStart string `json:"bucketStart"` + Count int64 `json:"count"` + + // Label Human-readable label (e.g. "Jan 1") + Label string `json:"label"` +} + // ProblemDetail defines model for ProblemDetail. type ProblemDetail struct { Detail string `json:"detail"` @@ -654,6 +720,12 @@ type GetRawJsonExportZipParams struct { XOdeVersion XOdeVersion `json:"x-ode-version"` } +// GetObservationStatsParams defines parameters for GetObservationStats. +type GetObservationStatsParams struct { + // XOdeVersion Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). + XOdeVersion XOdeVersion `json:"x-ode-version"` +} + // SyncPullParams defines parameters for SyncPull. type SyncPullParams struct { // SchemaType Filter by schemaType @@ -927,6 +999,9 @@ type ClientInterface interface { // GetRawJsonExportZip request GetRawJsonExportZip(ctx context.Context, params *GetRawJsonExportZipParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetObservationStats request + GetObservationStats(ctx context.Context, params *GetObservationStatsParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // SyncPullWithBody request with any body SyncPullWithBody(ctx context.Context, params *SyncPullParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1220,6 +1295,18 @@ func (c *Client) GetRawJsonExportZip(ctx context.Context, params *GetRawJsonExpo return c.Client.Do(req) } +func (c *Client) GetObservationStats(ctx context.Context, params *GetObservationStatsParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetObservationStatsRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) SyncPullWithBody(ctx context.Context, params *SyncPullParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewSyncPullRequestWithBody(c.Server, params, contentType, body) if err != nil { @@ -2319,6 +2406,46 @@ func NewGetRawJsonExportZipRequest(server string, params *GetRawJsonExportZipPar return req, nil } +// NewGetObservationStatsRequest generates requests for GetObservationStats +func NewGetObservationStatsRequest(server string, params *GetObservationStatsParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/stats/observations") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + if params != nil { + + var headerParam0 string + + headerParam0, err = runtime.StyleParamWithOptions("simple", false, "x-ode-version", params.XOdeVersion, runtime.StyleParamOptions{ParamLocation: runtime.ParamLocationHeader, Type: "string", Format: ""}) + if err != nil { + return nil, err + } + + req.Header.Set("x-ode-version", headerParam0) + + } + + return req, nil +} + // NewSyncPullRequest calls the generic SyncPull builder with application/json body func NewSyncPullRequest(server string, params *SyncPullParams, body SyncPullJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -2973,6 +3100,9 @@ type ClientWithResponsesInterface interface { // GetRawJsonExportZipWithResponse request GetRawJsonExportZipWithResponse(ctx context.Context, params *GetRawJsonExportZipParams, reqEditors ...RequestEditorFn) (*GetRawJsonExportZipHTTPResponse, error) + // GetObservationStatsWithResponse request + GetObservationStatsWithResponse(ctx context.Context, params *GetObservationStatsParams, reqEditors ...RequestEditorFn) (*GetObservationStatsHTTPResponse, error) + // SyncPullWithBodyWithResponse request with any body SyncPullWithBodyWithResponse(ctx context.Context, params *SyncPullParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SyncPullHTTPResponse, error) @@ -3422,6 +3552,31 @@ func (r GetRawJsonExportZipHTTPResponse) StatusCode() int { return 0 } +type GetObservationStatsHTTPResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ObservationStatsResponse + JSON401 *Unauthorized + JSON403 *Forbidden + JSON500 *InternalServerError +} + +// Status returns HTTPResponse.Status +func (r GetObservationStatsHTTPResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetObservationStatsHTTPResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type SyncPullHTTPResponse struct { Body []byte HTTPResponse *http.Response @@ -3872,6 +4027,15 @@ func (c *ClientWithResponses) GetRawJsonExportZipWithResponse(ctx context.Contex return ParseGetRawJsonExportZipHTTPResponse(rsp) } +// GetObservationStatsWithResponse request returning *GetObservationStatsHTTPResponse +func (c *ClientWithResponses) GetObservationStatsWithResponse(ctx context.Context, params *GetObservationStatsParams, reqEditors ...RequestEditorFn) (*GetObservationStatsHTTPResponse, error) { + rsp, err := c.GetObservationStats(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetObservationStatsHTTPResponse(rsp) +} + // SyncPullWithBodyWithResponse request with arbitrary body returning *SyncPullHTTPResponse func (c *ClientWithResponses) SyncPullWithBodyWithResponse(ctx context.Context, params *SyncPullParams, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*SyncPullHTTPResponse, error) { rsp, err := c.SyncPullWithBody(ctx, params, contentType, body, reqEditors...) @@ -4649,6 +4813,53 @@ func ParseGetRawJsonExportZipHTTPResponse(rsp *http.Response) (*GetRawJsonExport return response, nil } +// ParseGetObservationStatsHTTPResponse parses an HTTP response from a GetObservationStatsWithResponse call +func ParseGetObservationStatsHTTPResponse(rsp *http.Response) (*GetObservationStatsHTTPResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetObservationStatsHTTPResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ObservationStatsResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Unauthorized + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest Forbidden + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalServerError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseSyncPullHTTPResponse parses an HTTP response from a SyncPullWithResponse call func ParseSyncPullHTTPResponse(rsp *http.Response) (*SyncPullHTTPResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/synkronus-portal/eslint.config.js b/synkronus-portal/eslint.config.js index 4cb6e2ce7..b1fada31c 100644 --- a/synkronus-portal/eslint.config.js +++ b/synkronus-portal/eslint.config.js @@ -9,7 +9,7 @@ import prettierPlugin from 'eslint-plugin-prettier'; import { defineConfig, globalIgnores } from 'eslint/config'; export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(['dist', 'src/api/synkronus/generated/**']), { files: ['**/*.{ts,tsx}'], extends: [ diff --git a/synkronus-portal/package.json b/synkronus-portal/package.json index e8591f8a9..289336de9 100644 --- a/synkronus-portal/package.json +++ b/synkronus-portal/package.json @@ -23,7 +23,8 @@ "qrcode": "^1.5.4", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-icons": "^5.5.0" + "react-icons": "^5.5.0", + "recharts": "^3.10.1" }, "devDependencies": { "@eslint/js": "^9.39.2", diff --git a/synkronus-portal/pnpm-lock.yaml b/synkronus-portal/pnpm-lock.yaml index d6d78444d..d8e7f440b 100644 --- a/synkronus-portal/pnpm-lock.yaml +++ b/synkronus-portal/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: react-icons: specifier: ^5.5.0 version: 5.6.0(react@19.2.6) + recharts: + specifier: ^3.10.1 + version: 3.10.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@18.3.1)(react@19.2.6)(redux@5.0.1) devDependencies: '@eslint/js': specifier: ^9.39.2 @@ -564,6 +567,17 @@ packages: '@types/react': optional: true + '@reduxjs/toolkit@2.12.0': + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rolldown/pluginutils@1.0.0-rc.3': resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} @@ -708,6 +722,12 @@ packages: '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -730,6 +750,33 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -768,6 +815,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/wrap-ansi@3.0.0': resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} @@ -1057,6 +1107,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -1126,6 +1180,50 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + data-uri-to-buffer@6.0.2: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} @@ -1163,6 +1261,9 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1276,6 +1377,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.50.0: + resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} + esbuild@0.27.7: resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} @@ -1397,6 +1501,9 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} @@ -1645,6 +1752,9 @@ packages: engines: {node: '>=16.x'} hasBin: true + immer@11.1.15: + resolution: {integrity: sha512-VrNANlmnWQnh5COXIIOQXM9oOJw7naGKlBT74ZOOR6lpVXc3gFEu9FJLDFcpCJ2j+NWr8TIwtWD//T6ZX6TKiQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1660,6 +1770,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + invariant@2.2.4: resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} @@ -2280,6 +2394,18 @@ packages: '@types/react': optional: true + react-redux@9.3.0: + resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -2292,6 +2418,22 @@ packages: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} + recharts@3.10.1: + resolution: {integrity: sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -2313,6 +2455,9 @@ packages: require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -2525,6 +2670,9 @@ packages: throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} @@ -2635,10 +2783,18 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + utils-merge@1.0.1: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite@7.3.3: resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3270,6 +3426,18 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.15 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.2.0 + optionalDependencies: + react: 19.2.6 + react-redux: 9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1) + '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/rollup-android-arm-eabi@4.60.4': @@ -3349,6 +3517,10 @@ snapshots: '@sinclair/typebox@0.27.10': {} + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -3381,6 +3553,30 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + '@types/estree@1.0.8': {} '@types/estree@1.0.9': {} @@ -3421,6 +3617,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/use-sync-external-store@0.0.6': {} + '@types/wrap-ansi@3.0.0': {} '@types/yargs-parser@21.0.3': {} @@ -3778,6 +3976,8 @@ snapshots: clone@1.0.4: optional: true + clsx@2.1.1: {} + color-convert@2.0.1: dependencies: color-name: 1.1.4 @@ -3849,6 +4049,44 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-color@3.1.0: {} + + d3-ease@3.0.1: {} + + d3-format@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + data-uri-to-buffer@6.0.2: {} data-view-buffer@1.0.2: @@ -3879,6 +4117,8 @@ snapshots: decamelize@1.2.0: {} + decimal.js-light@2.5.1: {} + deep-is@0.1.4: {} defaults@1.0.4: @@ -4061,6 +4301,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.50.0: {} + esbuild@0.27.7: optionalDependencies: '@esbuild/aix-ppc64': 0.27.7 @@ -4230,6 +4472,8 @@ snapshots: event-target-shim@5.0.1: {} + eventemitter3@5.0.4: {} + exponential-backoff@3.1.3: {} fast-deep-equal@3.1.3: {} @@ -4484,6 +4728,8 @@ snapshots: dependencies: queue: 6.0.2 + immer@11.1.15: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -4499,6 +4745,8 @@ snapshots: hasown: 2.0.3 side-channel: 1.1.0 + internmap@2.0.3: {} + invariant@2.2.4: dependencies: loose-envify: 1.4.0 @@ -5259,12 +5507,47 @@ snapshots: - supports-color - utf-8-validate + react-redux@9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.6 + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + redux: 5.0.1 + react-refresh@0.14.2: {} react-refresh@0.18.0: {} react@19.2.6: {} + recharts@3.10.1(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react-is@18.3.1)(react@19.2.6)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1))(react@19.2.6) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.50.0 + eventemitter3: 5.0.4 + immer: 11.1.15 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-is: 18.3.1 + react-redux: 9.3.0(@types/react@19.2.14)(react@19.2.6)(redux@5.0.1) + reselect: 5.2.0 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.6) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect-metadata@0.2.2: {} reflect.getprototypeof@1.0.10: @@ -5293,6 +5576,8 @@ snapshots: require-main-filename@2.0.0: {} + reselect@5.2.0: {} + resolve-from@4.0.0: {} resolve@2.0.0-next.7: @@ -5583,6 +5868,8 @@ snapshots: throat@5.0.0: {} + tiny-invariant@1.3.3: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -5697,8 +5984,29 @@ snapshots: dependencies: punycode: 2.3.1 + use-sync-external-store@1.6.0(react@19.2.6): + dependencies: + react: 19.2.6 + utils-merge@1.0.1: {} + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + vite@7.3.3(@types/node@24.12.4)(terser@5.47.1)(yaml@2.9.0): dependencies: esbuild: 0.27.7 diff --git a/synkronus-portal/src/api/synkronus/generated/.openapi-generator/FILES b/synkronus-portal/src/api/synkronus/generated/.openapi-generator/FILES index 03184907e..a9761a534 100644 --- a/synkronus-portal/src/api/synkronus/generated/.openapi-generator/FILES +++ b/synkronus-portal/src/api/synkronus/generated/.openapi-generator/FILES @@ -35,7 +35,11 @@ docs/GetHealth503Response.md docs/HealthApi.md docs/LoginRequest.md docs/Observation.md +docs/ObservationFormTypeCount.md docs/ObservationGeolocation.md +docs/ObservationStatsResponse.md +docs/ObservationTimeline.md +docs/ObservationTimelineBucket.md docs/ProblemDetail.md docs/ProblemDetailErrorsInner.md docs/RefreshTokenRequest.md @@ -44,6 +48,7 @@ docs/RepositoryResetResponse.md docs/ResetUserPassword200Response.md docs/ResetUserPasswordRequest.md docs/ServerInfo.md +docs/StatsApi.md docs/SwitchAppBundleVersion200Response.md docs/SyncPullRequest.md docs/SyncPullRequestSince.md diff --git a/synkronus-portal/src/api/synkronus/generated/api.ts b/synkronus-portal/src/api/synkronus/generated/api.ts index f0e406027..8dde1b5a0 100644 --- a/synkronus-portal/src/api/synkronus/generated/api.ts +++ b/synkronus-portal/src/api/synkronus/generated/api.ts @@ -299,6 +299,13 @@ export interface Observation { */ 'tags'?: Array | null; } +export interface ObservationFormTypeCount { + /** + * Form type id; empty/whitespace stored values become \"(no form type)\" + */ + 'formType': string; + 'count': number; +} /** * Optional geolocation data for the observation */ @@ -328,6 +335,55 @@ export interface ObservationGeolocation { */ 'timestamp'?: string; } +export interface ObservationStatsResponse { + /** + * Total non-deleted observations + */ + 'totalCount': number; + 'byFormType': Array; + 'timeline': ObservationTimeline; + /** + * UTC timestamp when this aggregate was computed + */ + 'computedAt': string; +} +export interface ObservationTimeline { + /** + * Bucket granularity (week when span of dated observations is >= 365 days) + */ + 'bucketUnit': ObservationTimelineBucketUnitEnum; + /** + * First bucket start date (YYYY-MM-DD), empty when there are no observations + */ + 'rangeStart': string; + /** + * Last bucket start date (YYYY-MM-DD), empty when there are no observations + */ + 'rangeEnd': string; + /** + * Dense zero-filled buckets from rangeStart through rangeEnd + */ + 'buckets': Array; +} + +export const ObservationTimelineBucketUnitEnum = { + Day: 'day', + Week: 'week', +} as const; + +export type ObservationTimelineBucketUnitEnum = typeof ObservationTimelineBucketUnitEnum[keyof typeof ObservationTimelineBucketUnitEnum]; + +export interface ObservationTimelineBucket { + /** + * Bucket start date (YYYY-MM-DD, UTC) + */ + 'bucketStart': string; + /** + * Human-readable label (e.g. \"Jan 1\") + */ + 'label': string; + 'count': number; +} export interface ProblemDetail { 'type': string; 'title': string; @@ -3208,3 +3264,120 @@ export class HealthApi extends BaseAPI { +/** + * StatsApi - axios parameter creator + */ +export const StatsApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + * @summary Observation aggregate stats for dashboard charts + * @param {string} xOdeVersion Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getObservationStats: async (xOdeVersion: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'xOdeVersion' is not null or undefined + assertParamExists('getObservationStats', 'xOdeVersion', xOdeVersion) + const localVarPath = `/api/stats/observations`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearerAuth required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + localVarHeaderParameter['Accept'] = 'application/json'; + + if (xOdeVersion != null) { + localVarHeaderParameter['x-ode-version'] = String(xOdeVersion); + } + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * StatsApi - functional programming interface + */ +export const StatsApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = StatsApiAxiosParamCreator(configuration) + return { + /** + * Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + * @summary Observation aggregate stats for dashboard charts + * @param {string} xOdeVersion Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getObservationStats(xOdeVersion: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getObservationStats(xOdeVersion, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['StatsApi.getObservationStats']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * StatsApi - factory interface + */ +export const StatsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = StatsApiFp(configuration) + return { + /** + * Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + * @summary Observation aggregate stats for dashboard charts + * @param {StatsApiGetObservationStatsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getObservationStats(requestParameters: StatsApiGetObservationStatsRequest, options?: RawAxiosRequestConfig): AxiosPromise { + return localVarFp.getObservationStats(requestParameters.xOdeVersion, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * Request parameters for getObservationStats operation in StatsApi. + */ +export interface StatsApiGetObservationStatsRequest { + /** + * Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). + */ + readonly xOdeVersion: string +} + +/** + * StatsApi - object-oriented interface + */ +export class StatsApi extends BaseAPI { + /** + * Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + * @summary Observation aggregate stats for dashboard charts + * @param {StatsApiGetObservationStatsRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + public getObservationStats(requestParameters: StatsApiGetObservationStatsRequest, options?: RawAxiosRequestConfig) { + return StatsApiFp(this.configuration).getObservationStats(requestParameters.xOdeVersion, options).then((request) => request(this.axios, this.basePath)); + } +} + + + diff --git a/synkronus-portal/src/api/synkronus/generated/docs/ObservationFormTypeCount.md b/synkronus-portal/src/api/synkronus/generated/docs/ObservationFormTypeCount.md new file mode 100644 index 000000000..90eb22afa --- /dev/null +++ b/synkronus-portal/src/api/synkronus/generated/docs/ObservationFormTypeCount.md @@ -0,0 +1,22 @@ +# ObservationFormTypeCount + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**formType** | **string** | Form type id; empty/whitespace stored values become \"(no form type)\" | [default to undefined] +**count** | **number** | | [default to undefined] + +## Example + +```typescript +import { ObservationFormTypeCount } from './api'; + +const instance: ObservationFormTypeCount = { + formType, + count, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/synkronus-portal/src/api/synkronus/generated/docs/ObservationStatsResponse.md b/synkronus-portal/src/api/synkronus/generated/docs/ObservationStatsResponse.md new file mode 100644 index 000000000..1d3df849b --- /dev/null +++ b/synkronus-portal/src/api/synkronus/generated/docs/ObservationStatsResponse.md @@ -0,0 +1,26 @@ +# ObservationStatsResponse + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**totalCount** | **number** | Total non-deleted observations | [default to undefined] +**byFormType** | [**Array<ObservationFormTypeCount>**](ObservationFormTypeCount.md) | | [default to undefined] +**timeline** | [**ObservationTimeline**](ObservationTimeline.md) | | [default to undefined] +**computedAt** | **string** | UTC timestamp when this aggregate was computed | [default to undefined] + +## Example + +```typescript +import { ObservationStatsResponse } from './api'; + +const instance: ObservationStatsResponse = { + totalCount, + byFormType, + timeline, + computedAt, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/synkronus-portal/src/api/synkronus/generated/docs/ObservationTimeline.md b/synkronus-portal/src/api/synkronus/generated/docs/ObservationTimeline.md new file mode 100644 index 000000000..a6f87d58c --- /dev/null +++ b/synkronus-portal/src/api/synkronus/generated/docs/ObservationTimeline.md @@ -0,0 +1,26 @@ +# ObservationTimeline + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**bucketUnit** | **string** | Bucket granularity (week when span of dated observations is >= 365 days) | [default to undefined] +**rangeStart** | **string** | First bucket start date (YYYY-MM-DD), empty when there are no observations | [default to undefined] +**rangeEnd** | **string** | Last bucket start date (YYYY-MM-DD), empty when there are no observations | [default to undefined] +**buckets** | [**Array<ObservationTimelineBucket>**](ObservationTimelineBucket.md) | Dense zero-filled buckets from rangeStart through rangeEnd | [default to undefined] + +## Example + +```typescript +import { ObservationTimeline } from './api'; + +const instance: ObservationTimeline = { + bucketUnit, + rangeStart, + rangeEnd, + buckets, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/synkronus-portal/src/api/synkronus/generated/docs/ObservationTimelineBucket.md b/synkronus-portal/src/api/synkronus/generated/docs/ObservationTimelineBucket.md new file mode 100644 index 000000000..ad40928ed --- /dev/null +++ b/synkronus-portal/src/api/synkronus/generated/docs/ObservationTimelineBucket.md @@ -0,0 +1,24 @@ +# ObservationTimelineBucket + + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**bucketStart** | **string** | Bucket start date (YYYY-MM-DD, UTC) | [default to undefined] +**label** | **string** | Human-readable label (e.g. \"Jan 1\") | [default to undefined] +**count** | **number** | | [default to undefined] + +## Example + +```typescript +import { ObservationTimelineBucket } from './api'; + +const instance: ObservationTimelineBucket = { + bucketStart, + label, + count, +}; +``` + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/synkronus-portal/src/api/synkronus/generated/docs/StatsApi.md b/synkronus-portal/src/api/synkronus/generated/docs/StatsApi.md new file mode 100644 index 000000000..bedb39d2c --- /dev/null +++ b/synkronus-portal/src/api/synkronus/generated/docs/StatsApi.md @@ -0,0 +1,62 @@ +# StatsApi + +All URIs are relative to *http://localhost* + +|Method | HTTP request | Description| +|------------- | ------------- | -------------| +|[**getObservationStats**](#getobservationstats) | **GET** /api/stats/observations | Observation aggregate stats for dashboard charts| + +# **getObservationStats** +> ObservationStatsResponse getObservationStats() + +Returns non-deleted observation totals grouped by form type and a dense activity timeline bucketed by UTC calendar day (or week when the span is at least 365 days). Soft-deleted observations are excluded. Timeline dates use `created_at` interpreted in UTC. Intended for portal/desktop overview charts; not a general-purpose query API. + +### Example + +```typescript +import { + StatsApi, + Configuration +} from './api'; + +const configuration = new Configuration(); +const apiInstance = new StatsApi(configuration); + +let xOdeVersion: string; //Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). (default to undefined) + +const { status, data } = await apiInstance.getObservationStats( + xOdeVersion +); +``` + +### Parameters + +|Name | Type | Description | Notes| +|------------- | ------------- | ------------- | -------------| +| **xOdeVersion** | [**string**] | Client semantic version; the major segment must match the server. Optional leading v/V and semver pre-release/build suffixes are accepted (same rules as Synkronus). | defaults to undefined| + + +### Return type + +**ObservationStatsResponse** + +### Authorization + +[bearerAuth](../README.md#bearerAuth) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +|**200** | Observation aggregate stats | - | +|**401** | Unauthorized | - | +|**403** | Forbidden | - | +|**500** | Internal server error | - | + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + diff --git a/synkronus-portal/src/assets/nodata.png b/synkronus-portal/src/assets/nodata.png new file mode 100644 index 000000000..edecca246 Binary files /dev/null and b/synkronus-portal/src/assets/nodata.png differ diff --git a/synkronus-portal/src/components/HomePanel.tsx b/synkronus-portal/src/components/HomePanel.tsx new file mode 100644 index 000000000..3d855b414 --- /dev/null +++ b/synkronus-portal/src/components/HomePanel.tsx @@ -0,0 +1,129 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Button } from '@ode/components/react-web'; +import { HiArrowPath } from 'react-icons/hi2'; +import type { ObservationStatsResponse } from '../api/synkronus/generated'; +import { api } from '../services/api'; +import { formatOverviewCount } from '../lib/observationStatsCharts'; +import noDataIllustration from '../assets/nodata.png'; +import { ObservationFormTypeChart } from './charts/ObservationFormTypeChart'; +import { ObservationTimelineChart } from './charts/ObservationTimelineChart'; + +export function HomePanel() { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadStats = useCallback(async () => { + setLoading(true); + setError(null); + try { + const result = await api.getObservationStats(); + setStats(result); + } catch (err) { + setError( + err instanceof Error ? err.message : 'Failed to load observation stats', + ); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + let cancelled = false; + + void api + .getObservationStats() + .then(result => { + if (cancelled) return; + setStats(result); + setError(null); + }) + .catch(err => { + if (cancelled) return; + setError( + err instanceof Error + ? err.message + : 'Failed to load observation stats', + ); + }) + .finally(() => { + if (!cancelled) { + setLoading(false); + } + }); + + return () => { + cancelled = true; + }; + }, []); + + const total = stats?.totalCount ?? 0; + + return ( +
+
+
+

Data overview

+
+
+ +
+
+ + {error && ( +
+ {error} + +
+ )} + + {loading && !stats && ( +
+ Loading observation stats… +
+ )} + + {stats && !error && total === 0 && ( +
+ +

This server contains no observations yet

+
+ )} + + {stats && total > 0 && ( + <> +

+ {formatOverviewCount(total)} observation + {total === 1 ? '' : 's'} + {stats.computedAt + ? ` Β· updated ${new Date(stats.computedAt).toLocaleString()}` + : ''} +

+
+ + +
+ + )} +
+ ); +} diff --git a/synkronus-portal/src/components/charts/ObservationFormTypeChart.tsx b/synkronus-portal/src/components/charts/ObservationFormTypeChart.tsx new file mode 100644 index 000000000..3ebeb0698 --- /dev/null +++ b/synkronus-portal/src/components/charts/ObservationFormTypeChart.tsx @@ -0,0 +1,104 @@ +import { useMemo } from 'react'; +import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts'; +import type { ObservationFormTypeCount } from '../../api/synkronus/generated'; +import { + buildFormTypeChartSlices, + formatOverviewCount, +} from '../../lib/observationStatsCharts'; +import { OverviewChartPanel } from './OverviewChartPanel'; + +export interface ObservationFormTypeChartProps { + rows: ObservationFormTypeCount[]; + totalObservations: number; +} + +export function ObservationFormTypeChart({ + rows, + totalObservations, +}: ObservationFormTypeChartProps) { + const slices = useMemo(() => buildFormTypeChartSlices(rows), [rows]); + + if (totalObservations === 0 || slices.length === 0) { + return ( + +

+ No form types to display. +

+
+ ); + } + + return ( + +
+
+ + + + {slices.map(slice => ( + + ))} + + { + const count = + typeof value === 'number' ? value : Number(value ?? 0); + const pct = + totalObservations > 0 + ? ((count / totalObservations) * 100).toFixed(1) + : '0'; + const formType = + item && typeof item === 'object' && 'payload' in item + ? String( + (item.payload as { formType?: string }).formType ?? + '', + ) + : ''; + return [`${formatOverviewCount(count)} (${pct}%)`, formType]; + }} + /> + + +
+ + {formatOverviewCount(totalObservations)} + + total +
+
+
    + {slices.map(slice => ( +
  • + + + {slice.formType} + + + {formatOverviewCount(slice.count)} + +
  • + ))} +
+
+
+ ); +} diff --git a/synkronus-portal/src/components/charts/ObservationTimelineChart.tsx b/synkronus-portal/src/components/charts/ObservationTimelineChart.tsx new file mode 100644 index 000000000..4b4466c35 --- /dev/null +++ b/synkronus-portal/src/components/charts/ObservationTimelineChart.tsx @@ -0,0 +1,99 @@ +import { useMemo } from 'react'; +import { + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import type { ObservationTimeline } from '../../api/synkronus/generated'; +import { formatOverviewCount } from '../../lib/observationStatsCharts'; +import { OverviewChartPanel } from './OverviewChartPanel'; + +export interface ObservationTimelineChartProps { + timeline: ObservationTimeline; + totalObservations: number; +} + +export function ObservationTimelineChart({ + timeline, + totalObservations, +}: ObservationTimelineChartProps) { + const data = useMemo( + () => + timeline.buckets.map(b => ({ + label: b.label, + count: b.count, + })), + [timeline.buckets], + ); + + const subtitle = useMemo(() => { + if (!timeline.rangeStart || !timeline.rangeEnd) { + return undefined; + } + return `${timeline.rangeStart} β€” ${timeline.rangeEnd} Β· ${timeline.bucketUnit === 'week' ? 'weekly' : 'daily'} buckets`; + }, [timeline]); + + if (totalObservations === 0 || data.length === 0) { + return ( + +

+ Sync observations to see a timeline. +

+
+ ); + } + + return ( + + + + + + + { + const count = + typeof value === 'number' ? value : Number(value ?? 0); + return [formatOverviewCount(count), 'Observations']; + }} + labelFormatter={label => `Period: ${label}`} + /> + + + + + ); +} diff --git a/synkronus-portal/src/components/charts/OverviewChartPanel.tsx b/synkronus-portal/src/components/charts/OverviewChartPanel.tsx new file mode 100644 index 000000000..2ef4a2ce0 --- /dev/null +++ b/synkronus-portal/src/components/charts/OverviewChartPanel.tsx @@ -0,0 +1,27 @@ +import type { ReactNode } from 'react'; + +export interface OverviewChartPanelProps { + title: string; + subtitle?: string; + children: ReactNode; + className?: string; +} + +export function OverviewChartPanel({ + title, + subtitle, + children, + className, +}: OverviewChartPanelProps) { + return ( +
+
+

{title}

+ {subtitle ? ( +

{subtitle}

+ ) : null} +
+
{children}
+
+ ); +} diff --git a/synkronus-portal/src/contexts/AuthContext.tsx b/synkronus-portal/src/contexts/AuthContext.tsx index 2b9233f1d..f86e81a7a 100644 --- a/synkronus-portal/src/contexts/AuthContext.tsx +++ b/synkronus-portal/src/contexts/AuthContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState, useEffect } from 'react'; +import { createContext, useContext, useState } from 'react'; import type { ReactNode } from 'react'; import { api } from '../services/api'; import type { LoginRequest, User, AuthState } from '../types/auth'; @@ -11,37 +11,37 @@ interface AuthContextType extends AuthState { const AuthContext = createContext(undefined); -export function AuthProvider({ children }: { children: ReactNode }) { - const [authState, setAuthState] = useState({ +function readStoredAuthState(): AuthState { + const token = localStorage.getItem('token'); + const refreshToken = localStorage.getItem('refreshToken'); + const userStr = localStorage.getItem('user'); + + if (token && refreshToken && userStr) { + try { + const user = JSON.parse(userStr) as User; + return { + user, + token, + refreshToken, + isAuthenticated: true, + }; + } catch { + localStorage.removeItem('token'); + localStorage.removeItem('refreshToken'); + localStorage.removeItem('user'); + } + } + + return { user: null, token: null, refreshToken: null, isAuthenticated: false, - }); + }; +} - // Load auth state from localStorage on mount - useEffect(() => { - const token = localStorage.getItem('token'); - const refreshToken = localStorage.getItem('refreshToken'); - const userStr = localStorage.getItem('user'); - - if (token && refreshToken && userStr) { - try { - const user = JSON.parse(userStr); - setAuthState({ - user, - token, - refreshToken, - isAuthenticated: true, - }); - } catch { - // Invalid stored data, clear it - localStorage.removeItem('token'); - localStorage.removeItem('refreshToken'); - localStorage.removeItem('user'); - } - } - }, []); +export function AuthProvider({ children }: { children: ReactNode }) { + const [authState, setAuthState] = useState(readStoredAuthState); const login = async (credentials: LoginRequest) => { const response = await api.login(credentials); diff --git a/synkronus-portal/src/lib/observationStatsCharts.ts b/synkronus-portal/src/lib/observationStatsCharts.ts new file mode 100644 index 000000000..4bcd9ac3b --- /dev/null +++ b/synkronus-portal/src/lib/observationStatsCharts.ts @@ -0,0 +1,71 @@ +import type { ObservationFormTypeCount } from '../api/synkronus/generated'; + +/** Series colors tuned for the portal light/dark themes. */ +export const OVERVIEW_CHART_COLORS = [ + '#6b8ae8', + '#5cb88a', + '#e0a86a', + '#c77dff', + '#56cfe1', + '#ff8f8f', + '#90a3cb', + '#ffb95f', +] as const; + +export const OVERVIEW_CHART_OTHER_COLOR = '#5a6478'; + +const MAX_PIE_SLICES = 8; + +export interface FormTypeChartSlice { + formType: string; + count: number; + color: string; +} + +export function colorForFormType(formType: string, index: number): string { + if (formType.startsWith('Other')) { + return OVERVIEW_CHART_OTHER_COLOR; + } + return OVERVIEW_CHART_COLORS[index % OVERVIEW_CHART_COLORS.length]; +} + +/** Collapse form-type rows to at most 8 pie slices (top 7 + Other). */ +export function buildFormTypeChartSlices( + rows: ObservationFormTypeCount[], +): FormTypeChartSlice[] { + if (rows.length === 0) { + return []; + } + + const sorted = [...rows].sort((a, b) => b.count - a.count); + + if (sorted.length <= MAX_PIE_SLICES) { + return sorted.map((row, i) => ({ + formType: row.formType, + count: row.count, + color: colorForFormType(row.formType, i), + })); + } + + const head = sorted.slice(0, MAX_PIE_SLICES - 1); + const tail = sorted.slice(MAX_PIE_SLICES - 1); + const otherCount = tail.reduce((sum, row) => sum + row.count, 0); + + const slices: FormTypeChartSlice[] = head.map((row, i) => ({ + formType: row.formType, + count: row.count, + color: colorForFormType(row.formType, i), + })); + + slices.push({ + formType: `Other (${tail.length} types)`, + count: otherCount, + color: OVERVIEW_CHART_OTHER_COLOR, + }); + + return slices; +} + +export function formatOverviewCount(n: number): string { + return n.toLocaleString(); +} diff --git a/synkronus-portal/src/pages/Dashboard.css b/synkronus-portal/src/pages/Dashboard.css index bb298718b..ef682d751 100644 --- a/synkronus-portal/src/pages/Dashboard.css +++ b/synkronus-portal/src/pages/Dashboard.css @@ -169,6 +169,22 @@ html::-webkit-scrollbar-thumb:hover { position: relative; } +button.logo-home-link { + background: none; + border: none; + padding: 0; + cursor: pointer; + font: inherit; + color: inherit; + text-align: left; +} + +button.logo-home-link:focus-visible { + outline: 2px solid var(--color-brand-primary-500); + outline-offset: 4px; + border-radius: 8px; +} + .logo-icon { width: var(--icon-size-2xl); height: var(--icon-size-2xl); @@ -1324,7 +1340,6 @@ html::-webkit-scrollbar-thumb:hover { flex-direction: column; align-items: center; justify-content: flex-start; - max-width: 1000px; width: 100%; margin: 0 auto; margin-bottom: var(--spacing-8); /* Additional margin from bottom edge */ @@ -2186,7 +2201,6 @@ html::-webkit-scrollbar-thumb:hover { overflow: hidden; overflow-x: auto; width: 100%; - max-width: 1000px; margin: 0 auto; margin-top: var(--spacing-8); padding: 0; @@ -4656,3 +4670,220 @@ html::-webkit-scrollbar-thumb:hover { [data-theme='light'] .info-card:nth-child(even) .info-icon svg { color: var(--color-brand-primary-500); } + +/* ---- Home overview charts + footer ---- */ +.dashboard-footer { + text-align: center; + padding: var(--spacing-4) var(--spacing-6) var(--spacing-6); + margin-top: auto; +} + +.dashboard-footer .version-text { + font-size: var(--font-size-xs); + color: var(--color-text-tertiary); + font-weight: var(--font-weight-regular); + opacity: var(--opacity-7); + letter-spacing: var(--letter-spacing-wide); + text-transform: uppercase; +} + +[data-theme='light'] .dashboard-footer .version-text { + color: var(--color-text-tertiary-light); +} + +.home-section { + --home-chart-tick: #90a3cb; + --home-chart-axis: #2d3449; + --home-chart-tooltip-bg: #131b2e; + --home-chart-tooltip-border: #2d3449; + --home-chart-tooltip-fg: #e8edf8; + --home-chart-donut-stroke: var(--color-background-card, #131b2e); + width: 100%; +} + +[data-theme='light'] .home-section { + --home-chart-tick: #5a6a85; + --home-chart-axis: #c5cddb; + --home-chart-tooltip-bg: #ffffff; + --home-chart-tooltip-border: #d0d7e2; + --home-chart-tooltip-fg: #1a2333; + --home-chart-donut-stroke: #ffffff; +} + +.home-stats-loading { + padding: var(--spacing-8) 0; + text-align: center; +} + +.home-stats-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--spacing-3); + padding: var(--spacing-8) var(--spacing-4); + text-align: center; +} + +.home-stats-empty-illustration { + width: min(600px, 100%); + height: auto; + display: block; + margin: 0 auto var(--spacing-2); + user-select: none; + pointer-events: none; +} + +.home-stats-empty-title { + margin: 0; + font-size: var(--font-size-xl); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); +} + +.home-stats-empty-copy { + margin: 0; + max-width: 28rem; + font-size: var(--font-size-sm); +} + +.home-stats-error { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--spacing-3); + margin-bottom: var(--spacing-4); +} + +.home-stats-summary { + margin: 0 0 var(--spacing-4); + font-size: var(--font-size-sm); + color: var(--color-text-secondary); +} + +.home-overview-charts-row { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--spacing-4); + align-items: stretch; +} + +@media (max-width: 900px) { + .home-overview-charts-row { + grid-template-columns: 1fr; + } +} + +.home-overview-chart { + display: flex; + flex-direction: column; + min-height: 0; + margin: 0; + padding: var(--spacing-4); + border-radius: var(--border-radius-lg, 12px); + background: var(--color-background-card); + border: 1px solid var(--color-border-subtle, rgba(255, 255, 255, 0.08)); +} + +[data-theme='light'] .home-overview-chart { + border-color: var(--color-border-default); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); +} + +.home-overview-chart-header h3 { + margin: 0; + font-size: var(--font-size-sm); + font-weight: var(--font-weight-semibold); + color: var(--color-text-primary); +} + +.home-overview-chart-subtitle { + margin: 0.35rem 0 0; + font-size: var(--font-size-sm); +} + +.home-overview-chart-body { + margin-top: var(--spacing-3); + min-height: 0; +} + +.home-overview-chart-empty { + margin: 0; + padding: var(--spacing-6) 0; + text-align: center; +} + +.home-overview-donut-layout { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: var(--spacing-3); + align-items: center; +} + +@media (max-width: 640px) { + .home-overview-donut-layout { + grid-template-columns: 1fr; + } +} + +.home-overview-donut-wrap { + position: relative; + min-height: 240px; +} + +.home-overview-donut-center { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + pointer-events: none; +} + +.home-overview-donut-total { + font-size: 1.35rem; + font-weight: 700; + color: var(--color-text-primary); +} + +.home-overview-legend { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.45rem; + max-height: 240px; + overflow-y: auto; +} + +.home-overview-legend li { + display: grid; + grid-template-columns: 0.75rem 1fr auto; + gap: 0.5rem; + align-items: center; + font-size: var(--font-size-sm); +} + +.home-overview-legend-swatch { + width: 0.75rem; + height: 0.75rem; + border-radius: 999px; + flex-shrink: 0; +} + +.home-overview-legend-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.home-overview-legend-count { + font-variant-numeric: tabular-nums; +} + +.muted { + color: var(--color-text-tertiary); +} diff --git a/synkronus-portal/src/pages/Dashboard.tsx b/synkronus-portal/src/pages/Dashboard.tsx index 88613d404..a6ae1b590 100644 --- a/synkronus-portal/src/pages/Dashboard.tsx +++ b/synkronus-portal/src/pages/Dashboard.tsx @@ -16,6 +16,8 @@ import { HiCube, HiOutlineUsers, HiUsers, + HiOutlineHome, + HiHome, HiCheckCircle, HiExclamationTriangle, HiArrowUpTray, @@ -36,6 +38,7 @@ import { ColorBrandPrimary500 } from '@ode/tokens'; import portalLogo from '../assets/portal.png'; import dashboardBackgroundDark from '../assets/dashboard-background.png'; import dashboardBackgroundLight from '../assets/dashboard-background-light.png'; +import { HomePanel } from '../components/HomePanel'; import './Dashboard.css'; import type { UserListItem } from '../api/synkronus/generated'; @@ -49,7 +52,7 @@ function userMatchesSearch(u: UserListItem, query: string): boolean { ); } -type TabType = 'users' | 'app-bundles' | 'data-export'; +type TabType = 'home' | 'users' | 'app-bundles' | 'data-export'; interface AppBundleVersion { version: string; @@ -84,12 +87,13 @@ interface AppBundleVersionsResponse { export function Dashboard() { const { user, logout } = useAuth(); const { resolvedTheme } = useTheme(); - const [activeTab, setActiveTab] = useState('users'); + const [activeTab, setActiveTab] = useState('home'); const [appBundles, setAppBundles] = useState([]); const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [success, setSuccess] = useState(null); + const [serverVersion, setServerVersion] = useState(null); const [uploadProgress, setUploadProgress] = useState(0); const fileInputRef = useRef(null); /** Prevents overlapping listUsers calls without putting `loading` in loadUsers deps (which would retrigger the initial-load effect). */ @@ -362,15 +366,37 @@ export function Dashboard() { } }; - // Initial load: after login, the "users" tab is already active, but - // handleTabChange() won't run again. So explicitly fetch users when the - // logged-in user becomes an admin and the Users tab is active. + // Users load when switching to the Users tab (see handleTabChange). + // Home is the default tab, so no initial users fetch on mount. + useEffect(() => { - if (activeTab !== 'users') return; - if (user?.role !== 'admin') return; - if (users.length !== 0) return; - loadUsers(); - }, [activeTab, user?.role, users.length, loadUsers]); + const fetchVersion = async () => { + try { + const data = await api.getVersion(); + const version = data.server?.version || data.version; + if (version) { + setServerVersion(version); + return; + } + } catch { + // Continue to health fallback below. + } + + try { + const health = await api.getHealth(); + if (typeof health?.version === 'string') { + setServerVersion(health.version); + return; + } + } catch (err) { + console.debug('Failed to fetch server version:', err); + } + + setServerVersion('Unknown'); + }; + + void fetchVersion(); + }, []); const handleUploadClick = () => { if (loading) return; @@ -645,14 +671,18 @@ export function Dashboard() { }>
-
+
+
Welcome back: @@ -740,6 +770,44 @@ export function Dashboard() {