102 Commits

Author SHA1 Message Date
xbgmsharp
f56bb6f538 Add stats sql unit tests 2025-09-04 22:02:44 +02:00
xbgmsharp
d1578e3786 Update migration 202508 2025-09-04 21:58:45 +02:00
xbgmsharp
3bda948646 Release 0.9.4 2025-09-04 21:55:21 +02:00
xbgmsharp
45b2e3f28f Update docker image for tests 2025-09-04 21:54:52 +02:00
xbgmsharp
7ec74d7b82 Update tests versions, PostgREST 13.0.6 2025-09-04 21:52:19 +02:00
xbgmsharp
759a51d426 Update openapi documentation 2025-09-04 21:23:39 +02:00
xbgmsharp
0b76cb3d0f Update SQL corn tests unit 2025-09-04 21:23:04 +02:00
xbgmsharp
b4b0c1d014 Update migration 202508
- Improve Lint support
- Update new account email subject
- Update first badge message
- Add public.stay_delete_trigger_fn trigger function to delete stays_ext and process_queue entries
- Create trigger to delete stays_ext and process_queue entries
- Remove trigger that duplicate the OTP validation entry on insert for new account, it is handle by api.login
- Update api.login function to handle user disable and email verification, update error code with invalid_email_or_password
- Update cron_windy_fn to support custom user metrics
- Update merge_logbook_fn to handle more metrics and limit moorage deletion
- Add api.counts_fn to count logbook, moorages and stays entries
- Allow user_role to delete on api.stays_ext
2025-09-04 18:40:36 +02:00
xbgmsharp
ee44420e30 Merge pull request #22 from lokasoft/main
Added self hosted update guide
2025-08-31 09:20:05 +02:00
Lex
5f25a57c3c Added self hosted update guide 2025-08-30 11:51:36 +02:00
xbgmsharp
883c875e39 CLeanup 2025-08-28 18:37:45 +02:00
xbgmsharp
0282823938 Clean up 2025-08-28 18:37:29 +02:00
xbgmsharp
4546b75e0d Cleanup tests script 2025-08-28 18:37:11 +02:00
xbgmsharp
0c28ed6a0f Update docs README 2025-08-28 18:36:54 +02:00
xbgmsharp
57a754cdc0 Update tests versions, PostgREST 13.0.5 2025-08-27 22:39:45 +02:00
xbgmsharp
46f16fb077 Update README, add deepwiki 2025-08-24 09:43:23 +02:00
xbgmsharp
4c80d041cc Update tests versions, PostgreSQL 16.10, timescaledb 2.21.3 2025-08-18 19:36:57 +02:00
xbgmsharp
12e4baf662 Release PostgSail 0.9.3 2025-08-03 10:37:02 +02:00
xbgmsharp
faf62ed9a3 Update tests, PostgSail 0.9.3 2025-08-02 16:15:12 +02:00
xbgmsharp
40bdb9620f Update tests, PostgSail 0.9.3 2025-08-02 16:13:29 +02:00
xbgmsharp
8fe84ea80c Update migration 202507
- Update plugin upgrade message
- Update api.login, update the connected_at field to the current time
- Update monitoring_history_fn to use custom user settings for metrics
- Update cron_alerts_fn to check for alerts, filters out empty strings (""), so they are not included in the result.
- Update process_pre_logbook_fn to detect and avoid logbook we more than 1000NM in less 15h
2025-08-02 16:07:29 +02:00
xbgmsharp
54eefe582d Update migration 202505
- Update monitoring view to with true wind speed
2025-08-02 11:31:32 +02:00
xbgmsharp
d60af8c7b0 Update tests, TimescaleDB: 2.21.1 2025-08-02 10:40:43 +02:00
xbgmsharp
b505c98723 Update README 2025-08-02 10:32:14 +02:00
xbgmsharp
976ea85538 Update logbook tests 2025-07-14 08:56:48 +02:00
xbgmsharp
347af573c2 Update tests versions, timescaledb 2.21.0 2025-07-14 08:56:17 +02:00
xbgmsharp
60ba821af7 Update tests, add stays_ext unit tests 2025-06-22 10:42:33 +02:00
xbgmsharp
9759045b0a Update tests versions, PostgREST 13.0.4 2025-06-22 10:41:33 +02:00
xbgmsharp
a6c351c936 Update web UI front-end to v0.1.0-beta16 2025-06-21 13:00:39 +02:00
xbgmsharp
b7efb9636f Release 0.9.2 2025-06-15 22:22:55 +02:00
xbgmsharp
14d19a5394 Update migration 202505.
- Update metadata_ext table add REFERENCES and image_url
- Add stays_ext, new table to store vessel extended stays from user
- Update metadata_update_configuration_trigger, make sure it runs only when need.
- Create update_stays_ext_decode_base64_image_trigger_fn to decode base64 image
- Update function public.autodiscovery_config_fn and cron_process_autodiscovery_fn, hanlde no metric available
- Create api.stays_image to fetch stays image
- Update api.vessel_image to fetch boat image
- Update public.stay_active_geojson_fn function to produce a GeoJSON with the last position and stay details and anchor details
- Update api.monitoring_live, add anchor support
- Create view api.noteshistory_view, List stays and moorages notes order by stays
- Udpate public.process_stay_queue_fn, replace '0 day' stay name by 'short stay' name
- Create view api.logs_geojson_view, List logs with geojson
- Create view api.moorages_geojson_view, List moorages with geojson
- Create api.export_vessel_geojson_fn, export vessel as geojson
- Update api.export_logbook_geojson_linestring_trip_fn, add more trip properties to geojson
- Update api.export_logbook_geojson_trip_fn, add more trip properties to geojson
- Update api.export_logbooks_geojson_linestring_trips_fn, add more trip properties to geojson
- Update TABLE api.stays_ext with ROW LEVEL SECURITY
2025-06-15 22:13:53 +02:00
xbgmsharp
66b61f9d65 Update tests versions, timescaledb 2.20.3 2025-06-15 19:35:59 +02:00
xbgmsharp
b50c8f5007 Udpate REST API openAPI documentation 2025-06-09 10:19:44 +02:00
xbgmsharp
a9e1990184 Update postgsail ERD 2025-06-09 10:19:03 +02:00
xbgmsharp
59b52515e3 Update tests versions, timescaledb 2.20.2, PostgREST 13.0.2
Add stays_ext, metadata_ext table
2025-06-09 10:18:46 +02:00
xbgmsharp
f528456c08 Update migration 202505.
- Create trigger to process metadata for autodiscovery_config provisioning
- Create function public.autodiscovery_config_fn, to generate autodiscovery monitoring configuration
- Create cron_process_autodiscovery_fn to process autodiscovery config provisioning
- Refactor metadata_upsert_trigger_fn with the new metadata schema, remove id and check valid mmsi.
- Update email_templates table, add new autodiscovery template
2025-05-25 15:49:05 +02:00
xbgmsharp
a76c25b19f Update cron process output, add autodiscovery 2025-05-25 15:47:37 +02:00
xbgmsharp
00d2247549 Update cron job tests. Add support for autodiscovery 2025-05-25 10:57:38 +02:00
xbgmsharp
b84ac31da1 Update grafana tests 2025-05-25 10:57:03 +02:00
xbgmsharp
63cf5d24a5 Update tests versions, postgis 3.5.3 2025-05-25 10:55:42 +02:00
xbgmsharp
fe7c1dc1e5 Update metadata table test 2025-05-20 22:03:10 +02:00
xbgmsharp
10a26942c7 Update API schema 2025-05-20 22:02:30 +02:00
xbgmsharp
a5fb08fa42 Update ERD 2025-05-20 22:02:21 +02:00
xbgmsharp
25c74fd75a Update migration 202505
- Update metadata table coluonm type MMSI to text
- Update metadata upsert trigger, to ignore non-valid mmsi
- Update api.monitoring_live, fix invalid path tipo
- Update public.logbook_update_metrics_timebucket_fn, fix invalid path tipo and query
- Update public.logbook_update_metrics_fn, fix invalid path tipo
- Update public.logbook_update_metrics_short_fn, fix invalid path tipo
2025-05-20 21:58:00 +02:00
xbgmsharp
fa45782553 Update migration query order 2025-05-19 23:32:02 +02:00
xbgmsharp
e237391e8a Update API doc 2025-05-19 23:20:01 +02:00
xbgmsharp
dcf4eaca9b Add migration 202505
- Update metadata table, add IP address column, remove id column, update vessel_id default
- Add metadata_ext, new table to store vessel extended metadata from user
- Cleanup trigger on api schema, Move trigger on public schema
- Create trigger to update polar_updated_at and image_updated_at accordingly
- Create update_metadata_ext_decode_base64_image_trigger_fn to decode base64 image
- Refactor metadata_upsert_trigger_fn with the new metadata schema, remove id.
- Update metadata_grafana_trigger_fn with the new metadata schema, remove id.
- Create api.vessel_image to fetch vessel image
- Create api.vessel_extended_fn() to expose extended vessel details
- Update api.vessel_details_fn to use configuration metadata
- Update api.vessel_fn to use metadata_ext
- Update public.stay_active_geojson_fn function to produce a GeoJSON with the last position and stay details
- Update monitoring view to support live moorage in GeoJSON
- Update public.overpass_py_fn to check for seamark with name
- Update api.export_logbooks_geojson_linestring_trips_fn, add extra, _to_moorage_id, _from_moorage_id metadata
- Update api.monitoring_live, add live tracking view, Add support 6h outside barometer
- Update public.logbook_update_metrics_short_fn, aggregate more metrics and use user configuration
- Update public.logbook_update_metrics_fn, aggregate more metrics and use user configuration
- Update public.logbook_update_metrics_timebucket_fn, aggregate more metrics and use user configuration
- Update public.process_logbook_queue_fn to use new mobilitydb metrics
- Remove unnecessary functions
- Add missing comments on function
- Update public.cron_process_monitor_online_fn, refactor of metadata
- Update public.cron_process_monitor_offline_fn, Refactor metadata
- Update public.cron_process_grafana_fn, Refactor metadata
- Update permissions and RLS
- Update cron_process_skplugin_upgrade_fn, update check for signalk plugin version
2025-05-19 23:19:04 +02:00
xbgmsharp
ade15f538d Add more property in geojson linstring 2025-05-17 18:42:01 +02:00
xbgmsharp
f2cf604dab Add more property in geojson linstring 2025-05-17 17:22:51 +02:00
xbgmsharp
ad2e95bfa8 Update metadata tests, Refactor metadata table, Add metadata_ext tests 2025-05-17 17:22:24 +02:00
xbgmsharp
02130a9e4f Update grafana tests, refactor metadata table 2025-05-17 17:22:05 +02:00
xbgmsharp
29fa3863eb Update ERD, refactor metadata table 2025-05-17 17:20:46 +02:00
xbgmsharp
7cd06fced4 Update tests versions, timescaledb 2.20.0 2025-05-17 17:20:03 +02:00
xbgmsharp
564b85f58c Update postgsail ERD 2025-05-11 14:41:10 +02:00
xbgmsharp
da317dce87 Update tests, trying to improve overpass-api results 2025-05-11 14:30:38 +02:00
xbgmsharp
08ee757fa5 Update tests versions, PostgreSQL 16.9, PostgREST 12.3.0 2025-05-11 14:29:18 +02:00
xbgmsharp
76ade18d6b Update API doc 2025-05-11 14:27:03 +02:00
xbgmsharp
e6852a43f1 Update README 2025-05-11 14:26:49 +02:00
xbgmsharp
13f8240838 Release 0.9.1-202504 2025-05-04 22:41:27 +02:00
xbgmsharp
b7fe6a27b2 Update tests versions, PostgREST 12.2.12 2025-05-04 22:41:13 +02:00
xbgmsharp
29cc40f6de Update SQL comments 2025-05-04 22:32:42 +02:00
xbgmsharp
861e61d378 Update sql tests, name change in OSM data 2025-05-04 22:27:15 +02:00
xbgmsharp
684f34644f Update tests, improve and add more test 2025-05-04 22:26:37 +02:00
xbgmsharp
6ad9980cd2 Update tests, Update api.eventlogs_view skip the new_stay 2025-05-04 22:25:48 +02:00
xbgmsharp
7f5974efe2 Update tests, Remove deprecated client_id 2025-05-04 22:25:08 +02:00
xbgmsharp
f4b65d3156 Update README.md 2025-05-04 22:23:41 +02:00
xbgmsharp
d8ef8b8958 Add migration 202504
- Install and update TimescaleDB Toolkit
- Remove deprecated client_id
- Remove index from logbook columns
- Remove deprecated column from api.logbook
- Add new mobilityDB support with comments
- Update api.export_logbook_geojson_linestring_trip_fn, add more metadata properties
- Update public.check_jwt, Make new mobilitydb export geojson function anonymous access
- Create api.monitoring_upsert_fn, the function that update api.metadata monitoring configuration
- Update cron_process_skplugin_upgrade_fn, update check for signalk plugin version
- Update api.find_log_from_moorage_fn using the mobilitydb trajectory
- Update api.find_log_to_moorage_fn using the mobilitydb trajectory
- Update api.eventlogs_view to fetch the events logs backwards and skip the new_stay
- Update api.stats_logs_fn, ensure the trip is completed
- Update api.stats_fn, ensure the trip is completed
- Update moorage_delete_trigger_fn, When morrage is deleted, delete process_queue references as well.
- Create public.logbook_delete_trigger_fn, When logbook is deleted, logbook_ext need to deleted as well.
- Update metadata table, mark client_id as deprecated
- Update public.process_lat_lon_fn, Add new moorage refrence in public.process_queue for event logs review
- Add Description on missing trigger
- Update public.logbook_active_geojson_fn, fix log_gis_line as there is no end time yet
- Create public.stay_active_geojson_fn function to produce a GeoJSON with the last position and stay details
- Update monitoring view to support live moorage in GeoJSON
- Add api.monitoring_live view, the live tracking view
- Refresh permissions
2025-05-04 22:23:00 +02:00
xbgmsharp
686ac7498b Update web UI front-end to v0.1.0-beta15 2025-05-04 22:17:20 +02:00
xbgmsharp
d4c4347a4c Update web UI front-end to v0.1.0-beta14 2025-04-26 10:20:06 +02:00
xbgmsharp
4aacae3913 Update tests versions, PostgREST 12.2.11 2025-04-26 10:06:48 +02:00
xbgmsharp
06fd834441 Update metadata tests, ensure a reuslt for api.metadata.configuration base on update_at value 2025-04-21 23:42:12 +02:00
xbgmsharp
86bd4b5843 Update SQL tests, update event logs view revert b6fef6358a 2025-04-21 23:40:54 +02:00
xbgmsharp
111d7d36db Update tests versions, timescaledb_toolkit 1.21.0, timescaledb 2.19.3, PostgREST 12.2.10 2025-04-21 22:32:00 +02:00
xbgmsharp
b6fef6358a Update SQL tets, update event logs view 2025-04-13 08:35:06 +02:00
xbgmsharp
4aecea7532 Update tests versions, timescaledb 2.19.2 2025-04-13 08:34:28 +02:00
xbgmsharp
c8908748f7 Update Update ERD
- update logbook metrics
2025-04-06 22:36:40 +02:00
xbgmsharp
0e812c0939 Update mobilityDB unit sql tests, new geojson properties 2025-04-06 09:26:55 +02:00
xbgmsharp
395b7cfad7 Update sql unit test, Improve anymous test 2025-04-06 00:00:26 +02:00
xbgmsharp
14e8c8363c Update OpenAPI 2025-04-05 23:59:52 +02:00
xbgmsharp
448124f01b Update Update ERD
- Deprecated client_id
- Add new logbook metrics
2025-04-05 23:58:46 +02:00
xbgmsharp
3b466e3d93 Update tests versions, Add timescaledb_toolkit 1.19.0 2025-04-04 14:17:58 +02:00
xbgmsharp
f0ddca7d58 Update tests versions, timescaledb 2.19.1 2025-04-04 14:16:30 +02:00
xbgmsharp
7744ad4af9 Release 0.9.0-202503 2025-03-31 17:38:52 +02:00
xbgmsharp
5c70a9a453 Update cron_post_jobs output 2025-03-31 13:37:42 +02:00
xbgmsharp
6635015dbf Update mobilitydb SQL test 2025-03-31 13:37:26 +02:00
xbgmsharp
f285fcddb0 Add migration 202503
- Update metadata table, mark client_id as deprecated
- Update metadata table update configuration column type to jsonb
- Update metadata table add new column available_keys
- Update metadata_upsert_trigger_fn to metadata table
- Add metadata table tigger for update_metadata_configuration
- Update api.export_logbook_geojson_linestring_trip_fn, add metadata
- Add public.get_season, return the season based on the input date for logbook tag
- Refresh permissions
2025-03-31 13:34:39 +02:00
xbgmsharp
cabf405648 Update mobilitydb SQL test 2025-03-31 13:32:56 +02:00
xbgmsharp
b2f3372b26 Update API tests
- Deprecated client_id
- Update configuration
- Add available_keys
2025-03-31 13:32:19 +02:00
xbgmsharp
754c9bb6e7 Update tests versions, PostgSail 0.9.0, timescaledb 2.19.0 2025-03-31 13:30:12 +02:00
xbgmsharp
f9238c62dd Update OpenAPI 2025-03-31 13:29:22 +02:00
xbgmsharp
4a294674e8 Add metadata SQL unit test 2025-03-31 13:29:03 +02:00
xbgmsharp
83e92cfd6c Add metadata SQL test 2025-03-31 13:28:42 +02:00
xbgmsharp
718ca6d6ea Update grafana SQL tests
- Deprecated client_id
- Update configuration
- Add available_keys
2025-03-31 13:28:17 +02:00
xbgmsharp
a07f4f181c Update Update ERD
- Deprecated client_id
- Update configuration
- Add available_keys
2025-03-31 13:27:09 +02:00
xbgmsharp
72b06f9eb9 Update README 2025-03-14 16:01:23 +01:00
xbgmsharp
598a789d36 Update docker compose file, remove deprecated version 2025-02-27 15:19:11 +01:00
xbgmsharp
37e948cb20 Update tests versions, PostgreSQL 16.8, timescaledb 2.18.2 PostgREST 12.2.8 2025-02-27 15:17:53 +01:00
xbgmsharp
f26ece878b Update tests versions, PostGIS 3.5.2, timescaledb 2.18.0, PostgREST 12.2.6 2025-02-02 09:47:07 +01:00
xbgmsharp
9f8b43577e Update tests 2025-01-16 11:05:46 +01:00
xbgmsharp
0c76edf793 Update tests 2025-01-16 10:31:20 +01:00
xbgmsharp
0cac828347 Update front-end to v0.1.0-beta14 2025-01-16 10:30:38 +01:00
xbgmsharp
9e9189ac36 Update migration 202412:
- Update public.logbook_update_metrics_short_fn, handle corner use case
- Update public.logbook_update_metrics_fn, handle corner use case
- Update public.logbook_update_metrics_timebucket_fn, handle corner use case
2025-01-16 08:59:50 +01:00
42 changed files with 7492 additions and 334 deletions

View File

@@ -24,6 +24,8 @@
<a href="https://github.com/sponsors/xbgmsharp">Sponsors</a>
.
<a href="https://discord.gg/uuZrwz4dCS">Discord</a>
.
<a href="https://deepwiki.com/xbgmsharp/postgsail/">DeepWiki</a>
</p>
</p>
@@ -32,19 +34,24 @@
[![issues - postgsail](https://img.shields.io/github/issues/xbgmsharp/postgsail)](https://github.com/xbgmsharp/postgsail/issues)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
![Contributors](https://img.shields.io/github/contributors/xbgmsharp/postgsail?color=dark-green)
[![GitHub Repo stars](https://img.shields.io/github/stars/xbgmsharp/postgsail?style=social)](https://github.com/xbgmsharp/postgsail/stargazers)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/xbgmsharp/postgsail)
[![Test services db, api](https://github.com/xbgmsharp/postgsail/actions/workflows/db-test.yml/badge.svg)](https://github.com/xbgmsharp/postgsail/actions/workflows/db-test.yml)
[![Test services db, api, web](https://github.com/xbgmsharp/postgsail/actions/workflows/frontend-test.yml/badge.svg)](https://github.com/xbgmsharp/postgsail/actions/workflows/frontend-test.yml)
[![Test services db, grafana](https://github.com/xbgmsharp/postgsail/actions/workflows/grafana-test.yml/badge.svg)](https://github.com/xbgmsharp/postgsail/actions/workflows/grafana-test.yml)
signalk-postgsail:
[![GitHub Release](https://img.shields.io/github/release/xbgmsharp/signalk-postgsail.svg)](https://github.com/xbgmsharp/signalk-postgsail/releases/latest)
[![GitHub Release](https://img.shields.io/github/release/xbgmsharp/signalk-postgsail.svg?color=blue)](https://github.com/xbgmsharp/signalk-postgsail/releases/latest)
postgsail-backend:
[![GitHub Release](https://img.shields.io/github/release/xbgmsharp/postgsail.svg?color=blue)](https://github.com/xbgmsharp/postgsail/releases/latest)
postgsail-frontend:
[![GitHub Release](https://img.shields.io/github/release/xbgmsharp/vuestic-postgsail.svg)](https://github.com/xbgmsharp/vuestic-postgsail/releases/latest)
[![GitHub Release](https://img.shields.io/github/release/xbgmsharp/vuestic-postgsail.svg?color=blue)](https://github.com/xbgmsharp/vuestic-postgsail/releases/latest)
postgsail-telegram-bot:
[![GitHub Release](https://img.shields.io/github/release/xbgmsharp/postgsail-telegram-bot.svg)](https://github.com/xbgmsharp/postgsail-telegram-bot/releases/latest)
[![GitHub Release](https://img.shields.io/github/release/xbgmsharp/postgsail-telegram-bot.svg?color=blue)](https://github.com/xbgmsharp/postgsail-telegram-bot/releases/latest)
[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8124/badge)](https://www.bestpractices.dev/projects/8124)
@@ -55,7 +62,7 @@ postgsail-telegram-bot:
- [About The Project](#about-the-project)
- [Features](#features)
- [Cloud-hosted PostgSail](#cloud-hosted-postgsail)
- [On-Premise (for free)](#on-premise-for-free)
- [On-Premise](#on-premise)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [Creating A Pull Request](#creating-a-pull-request)
@@ -104,10 +111,16 @@ To understand the why and how, you might want to read [Why.md](https://github.co
Remove the hassle of running PostgSail yourself. Here you can skip the technical setup, the maintenance work and server costs by getting PostgSail on our reliable and secure PostgSail Cloud. Register and try for free at [iot.openplotter.cloud](https://iot.openplotter.cloud/).
## On-Premise (for free)
PostgSail Cloud is Open Source and free for personal use with a single vessel. If wish to manage multiple boats contact us.
PostgSail is free to use, but is not free to make or host. The stability and accuracy of PostgSail depends on its volunteers and donations from its users. Please consider making an annual recurring gift to PostgSail.
## On-Premise
Self host postgSail where you want and how you want. There are no restrictions, youre in full control. [Install Guide](https://github.com/xbgmsharp/postgsail/blob/main/docs/README.md)
PostgSail is free to use, but is not free to make or host. The stability and accuracy of PostgSail depends on its volunteers and donations from its users. Please consider making an annual recurring gift to PostgSail.
## Roadmap
See the [open issues](https://github.com/xbgmsharp/postgsail/issues) for a list of proposed features (and known issues).
@@ -142,5 +155,6 @@ An out of the box IoT platform using Docker (could be extend to K3 or K8) with t
- [PostgreSQL, open source object-relational database system](https://postgresql.org)
- [TimescaleDB, Time-series data extends PostgreSQL](https://www.timescale.com)
- [PostGIS, a spatial database extender for PostgreSQL object-relational database.](https://postgis.net/)
- [MobilityDB, An open source geospatial trajectory data management & analysis platform.](https://mobilitydb.com/)
- [Grafana, open observability platform | Grafana Labs](https://grafana.com)
- And many more

View File

@@ -1,5 +1,3 @@
version: "3.9"
services:
db:
image: xbgmsharp/timescaledb-postgis

View File

@@ -26,11 +26,15 @@ erDiagram
tfloat trip_batt_voltage "Battery Voltage"
tfloat trip_cog "courseovergroundtrue"
tfloat trip_depth "Depth"
tfloat trip_heading "heading True"
tfloat trip_hum_out "Humidity outside"
ttext trip_notes
tfloat trip_pres_out "Pressure outside"
tfloat trip_sog "speedoverground"
tfloat trip_solar_power "solar powerPanel"
tfloat trip_solar_voltage "solar voltage"
ttext trip_status
tfloat trip_tank_level "Tank currentLevel"
tfloat trip_temp_out "Temperature outside"
tfloat trip_temp_water "Temperature water"
tfloat trip_twa "windspeedapparent"
@@ -42,14 +46,17 @@ erDiagram
api_metadata {
boolean active "trigger monitor online/offline"
boolean active
jsonb available_keys "Signalk paths with unit for custom mapping"
jsonb available_keys
double_precision beam
text client_id
text configuration
jsonb configuration "Signalk path mapping for metrics"
jsonb configuration
timestamp_with_time_zone created_at "{NOT_NULL}"
double_precision height
integer id "{NOT_NULL}"
text ip "Store vessel ip address"
text ip
double_precision length
numeric mmsi
text mmsi
text name
text platform
text plugin_version "{NOT_NULL}"
@@ -61,9 +68,22 @@ erDiagram
text vessel_id "{NOT_NULL}"
}
api_metadata_ext {
timestamp_with_time_zone created_at "{NOT_NULL}"
bytea image "Store user boat image in bytea format"
text image_b64
text image_type "Store user boat image type in text format"
timestamp_with_time_zone image_updated_at
text image_url
text make_model "Store user make & model in text format"
text polar "Store polar data in CSV notation as used on ORC sailboat data"
timestamp_with_time_zone polar_updated_at
text vessel_id "{NOT_NULL}"
}
api_metrics {
double_precision anglespeedapparent
text client_id
text client_id "Deprecated client_id to be removed"
double_precision courseovergroundtrue
double_precision latitude "With CONSTRAINT but allow NULL value to be ignored silently by trigger"
double_precision longitude "With CONSTRAINT but allow NULL value to be ignored silently by trigger"
@@ -111,6 +131,17 @@ erDiagram
integer stay_code "{NOT_NULL}"
}
api_stays_ext {
timestamp_with_time_zone created_at "{NOT_NULL}"
bytea image "Store stays image in bytea format"
text image_b64
text image_type "Store stays image type in text format"
timestamp_with_time_zone image_updated_at
text image_url
integer stay_id "{NOT_NULL}"
text vessel_id "{NOT_NULL}"
}
auth_accounts {
timestamp_with_time_zone connected_at "{NOT_NULL}"
timestamp_with_time_zone created_at "{NOT_NULL}"
@@ -269,12 +300,15 @@ erDiagram
api_logbook }o--|| api_moorages : ""
api_logbook }o--|| api_moorages : ""
api_metadata }o--|| auth_vessels : ""
api_metadata_ext |o--|| api_metadata : ""
api_metrics }o--|| api_metadata : ""
api_moorages }o--|| api_metadata : ""
api_stays }o--|| api_metadata : ""
api_stays_ext }o--|| api_metadata : ""
api_moorages }o--|| api_stays_at : ""
api_stays }o--|| api_moorages : ""
api_stays }o--|| api_stays_at : ""
api_stays_ext |o--|| api_stays : ""
auth_otp |o--|| auth_accounts : ""
auth_vessels }o--|| auth_accounts : ""
```

View File

@@ -0,0 +1,277 @@
# Self hosted update guide
In this guide we are updating a self hosted installation version 0.7.2 to version 0.9.3. When updating from or to other versions principle remain the same.
The installation we are upgrading was installed in April 2024 using the installation instructions found on the pgsail github site. Platform is an Ubuntu 22.04 Virtual Machine.
Before the upgrade, around 120 trips were logged. Needless to say we don't want to loose our data.
Unfortunately, there is no automatic update path available, this may change but for now we had to follow the general update instuctions.
## General update instructions
- Make a backup
- Update the containers.
- Update possible extensions.
- Run database migrations.
- Additional data migration.
- Update SignalK client.
## Let's go
### Tools used
In addition to the tools that are already installed as part of Unbuntu and PostgSail, I used DBeaver to examine the database from my Windows desktop.
<https://dbeaver.io/download/>
### Make a backup
Start by making a backup of the database, the docker-compose.yml and .env files. Note that in my case the database was stored in a host folder, later versions are using a docker volume. To copy the database it neccesary the containers are stopped.
```bash
cd postgsail
mkdir backup
docker compose stop
cp .env docker-compose.yml backup/
docker compose cp -a db:/var/lib/postgresql/data backup/db-data
```
### Update the containers
Make a note of the last migration in the initdb folder, in my case this was 99_migrations_202404.sql. Because I used git clone, the migration file was a bit inbetween 0.7.1 and 0.7.2, therefore I decided 99_migrations_202404.sql was the first migration to run.
Remove the containers:
```bash
docker compose down
```
Get the latest PostgSail from github, we checkout a specific tag to ensure we have a stable release version. If you installed it from a binary release, just update from the latest binary release.
```bash
git pull remote main
git fetch --all --tags
git checkout tags/v0.9.3
```
```text
Note: switching to 'tags/v0.9.3'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at 12e4baf Release PostgSail 0.9.3
```
**Ensure new docker-compose.yml file matches your database folder or volume setting, adjust as needed.**
Get the latest containers.
```bash
docker compose pull
```
### Update possible extentions
Start database container.
```bash
docker compose up -d db
```
Excec psql shell in databse container.
```bash
docker compose exec db sh
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB"
\c signalk;
```
Check extensions which can be updated, be sure to run from the signalk database:
```sql
SELECT name, default_version, installed_version FROM pg_available_extensions where default_version <> installed_version;
```
The postgis extention can be upgraded with this SQL query:
```sql
SELECT postgis_extensions_upgrade();
```
Updating the timescaledb requires running from a new session, use following commands (note the -X options, that is neccesary):
```bash
docker compose exec db sh
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" -X
```
Then run following SQL commands from the psql shell:
```sql
ALTER EXTENSION timescaledb UPDATE;
CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit;
ALTER EXTENSION timescaledb_toolkit UPDATE;
```
For others, to be checked. In my case, the postgis extension was essential.
### Run datbabase migrations
Then run the migrations, adjust start and end for first and last migration file to execute.
```bash
start=202404; end=202507; for f in $(ls ./docker-entrypoint-initdb.d/99_migrations_*.sql | sort); do s=$(basename "$f" | sed -E 's/^99_migrations_([0-9]{6})\.sql$/\1/'); if [[ "$s" < "$start" || "$s" > "$end" ]]; then continue; fi; echo "Running $f"; psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < "$f"; done
```
Or line by line
```bash
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202404.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202405.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202406.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202407.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202408.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202409.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202410.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202411.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202412.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202501.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202504.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202505.sql
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" < ./docker-entrypoint-initdb.d/99_migrations_202507.sql
```
Now rebuild the web app.
```bash
docker compose build web
```
Maybe need to run 99env.sh - check.
Then we can start the other containers.
```bash
docker compose up -d
```
After everything is started, the web site should be accesible.
### Additional data migration
Depending on the starting version, additional data migration may be needed.
If the old trips are visible, but the routes are not, we need to run an SQL Script to re-calculate the trip metadata.
```sql
DO $$
declare
-- Re calculate the trip metadata
logbook_rec record;
avg_rec record;
t_rec record;
batch_size INTEGER := 20;
offset_value INTEGER := 0;
done BOOLEAN := FALSE;
processed INTEGER := 0;
begin
WHILE NOT done LOOP
processed := 0;
FOR logbook_rec IN
SELECT *
FROM api.logbook
WHERE _from IS NOT NULL
AND _to IS NOT NULL
AND active IS FALSE
AND trip IS NULL
--AND trip_heading IS NULL
--AND vessel_id = '06b6d311ccfe'
ORDER BY id DESC
LIMIT batch_size -- OFFSET offset_value -- don's use offset as causes entries to skip
LOOP
processed := processed + 1;
-- Update logbook entry with the latest metric data and calculate data
PERFORM set_config('vessel.id', logbook_rec.vessel_id, false);
-- Calculate trip metadata
avg_rec := logbook_update_avg_fn(logbook_rec.id, logbook_rec._from_time::TEXT, logbook_rec._to_time::TEXT);
--UPDATE api.logbook
-- SET extra = jsonb_recursive_merge(extra, jsonb_build_object('avg_wind_speed', avg_rec.avg_wind_speed))
-- WHERE id = logbook_rec.id;
if avg_rec.count_metric IS NULL OR avg_rec.count_metric = 0 then
-- We don't have the orignal metrics, we should read the geojson
continue; -- return current row of SELECT
end if;
-- mobilitydb, add spaciotemporal sequence
-- reduce the numbers of metrics by skipping row or aggregate time-series
-- By default the signalk plugin report one entry every minute.
IF avg_rec.count_metric < 30 THEN -- if less ~20min trip we keep it all data
t_rec := logbook_update_metrics_short_fn(avg_rec.count_metric, logbook_rec._from_time, logbook_rec._to_time);
ELSIF avg_rec.count_metric < 2000 THEN -- if less ~33h trip we skip data
t_rec := logbook_update_metrics_fn(avg_rec.count_metric, logbook_rec._from_time, logbook_rec._to_time);
ELSE -- As we have too many data, we time-series aggregate data
t_rec := logbook_update_metrics_timebucket_fn(avg_rec.count_metric, logbook_rec._from_time, logbook_rec._to_time);
END IF;
--RAISE NOTICE 'mobilitydb [%]', t_rec;
IF t_rec.trajectory IS NULL THEN
RAISE WARNING '-> process_logbook_queue_fn, vessel_id [%], invalid mobilitydb data [%] [%]', logbook_rec.vessel_id, _id, t_rec;
RETURN;
END IF;
RAISE NOTICE '-> process_logbook_queue_fn, vessel_id [%], update entry logbook id:[%] start:[%] end:[%]', logbook_rec.vessel_id, logbook_rec.id, logbook_rec._from_time, logbook_rec._to_time;
UPDATE api.logbook
SET
trip = t_rec.trajectory,
trip_cog = t_rec.courseovergroundtrue,
trip_sog = t_rec.speedoverground,
trip_twa = t_rec.windspeedapparent,
trip_tws = t_rec.truewindspeed,
trip_twd = t_rec.truewinddirection,
trip_notes = t_rec.notes, -- don't overwrite existing user notes. **** Must set trip_notes otherwise replay is not working.
trip_status = t_rec.status,
trip_depth = t_rec.depth,
trip_batt_charge = t_rec.stateofcharge,
trip_batt_voltage = t_rec.voltage,
trip_temp_water = t_rec.watertemperature,
trip_temp_out = t_rec.outsidetemperature,
trip_pres_out = t_rec.outsidepressure,
trip_hum_out = t_rec.outsidehumidity,
trip_heading = t_rec.heading, -- heading True
trip_tank_level = t_rec.tankLevel, -- Tank currentLevel
trip_solar_voltage = t_rec.solarVoltage, -- solar voltage
trip_solar_power = t_rec.solarPower -- solar powerPanel
WHERE id = logbook_rec.id;
END LOOP;
RAISE NOTICE '-> Processed:[%]', processed;
IF processed = 0 THEN
done := TRUE;
ELSE
offset_value := offset_value + batch_size;
END IF;
END LOOP;
END $$;
```
### Update SignalK client
The SignalK client can be updated from the SignalK Web UI. After the migration we updated this to version v0.5.0
### Trouble shooting
During this migration, several issues came up, they eventually boiled down to an extension not updated and permissions issues.

View File

@@ -248,7 +248,7 @@ RETURNS TABLE (
) AS $$
DECLARE
BEGIN
-- Aggregate all metrics as trip ios short.
-- Aggregate all metrics as trip is short.
RETURN QUERY
WITH metrics AS (
-- Extract metrics
@@ -267,7 +267,16 @@ BEGIN
COALESCE((m.metrics->'environment.outside.relativeHumidity')::NUMERIC, NULL) as outsidehumidity,
COALESCE((m.metrics->'environment.outside.pressure')::NUMERIC, NULL) as outsidepressure,
COALESCE((m.metrics->'environment.outside.temperature')::NUMERIC, NULL) as outsidetemperature,
COALESCE((m.metrics->'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC, NULL) as stateofcharge,
COALESCE(
NULLIF(
CASE
WHEN (m.metrics->>'electrical.batteries.House.capacity.stateOfCharge') ~ '^-?[0-9]+(\.[0-9]+)?$' THEN
(m.metrics->>'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC
END,
NULL
),
NULL
) as stateofcharge,
COALESCE((m.metrics->'electrical.batteries.House.voltage')::NUMERIC, NULL) as voltage,
ST_MakePoint(m.longitude, m.latitude) AS geo_point
FROM api.metrics m
@@ -362,7 +371,16 @@ BEGIN
COALESCE((t.metrics->'environment.outside.relativeHumidity')::NUMERIC, NULL) as outsidehumidity,
COALESCE((t.metrics->'environment.outside.pressure')::NUMERIC, NULL) as outsidepressure,
COALESCE((t.metrics->'environment.outside.temperature')::NUMERIC, NULL) as outsidetemperature,
COALESCE((t.metrics->'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC, NULL) as stateofcharge,
COALESCE(
NULLIF(
CASE
WHEN (t.metrics->>'electrical.batteries.House.capacity.stateOfCharge') ~ '^-?[0-9]+(\.[0-9]+)?$' THEN
(t.metrics->>'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC
END,
NULL
),
NULL
) as stateofcharge,
COALESCE((t.metrics->'electrical.batteries.House.voltage')::NUMERIC, NULL) as voltage,
ST_MakePoint(t.longitude, t.latitude) AS geo_point
FROM (
@@ -395,7 +413,16 @@ BEGIN
COALESCE((m.metrics->'environment.outside.relativeHumidity')::NUMERIC, NULL) as outsidehumidity,
COALESCE((m.metrics->'environment.outside.pressure')::NUMERIC, NULL) as outsidepressure,
COALESCE((m.metrics->'environment.outside.temperature')::NUMERIC, NULL) as outsidetemperature,
COALESCE((m.metrics->'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC, NULL) as stateofcharge,
COALESCE(
NULLIF(
CASE
WHEN (m.metrics->>'electrical.batteries.House.capacity.stateOfCharge') ~ '^-?[0-9]+(\.[0-9]+)?$' THEN
(m.metrics->>'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC
END,
NULL
),
NULL
) as stateofcharge,
COALESCE((m.metrics->'electrical.batteries.House.voltage')::NUMERIC, NULL) as voltage,
ST_MakePoint(m.longitude, m.latitude) AS geo_point
FROM api.metrics m
@@ -424,7 +451,16 @@ BEGIN
COALESCE((m.metrics->'environment.outside.relativeHumidity')::NUMERIC, NULL) as outsidehumidity,
COALESCE((m.metrics->'environment.outside.pressure')::NUMERIC, NULL) as outsidepressure,
COALESCE((m.metrics->'environment.outside.temperature')::NUMERIC, NULL) as outsidetemperature,
COALESCE((m.metrics->'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC, NULL) as stateofcharge,
COALESCE(
NULLIF(
CASE
WHEN (m.metrics->>'electrical.batteries.House.capacity.stateOfCharge') ~ '^-?[0-9]+(\.[0-9]+)?$' THEN
(m.metrics->>'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC
END,
NULL
),
NULL
) as stateofcharge,
COALESCE((m.metrics->'electrical.batteries.House.voltage')::NUMERIC, NULL) as voltage,
ST_MakePoint(m.longitude, m.latitude) AS geo_point
FROM api.metrics m
@@ -513,7 +549,7 @@ BEGIN
RETURN QUERY
WITH metrics AS (
-- Extract metrics base the total of entry ignoring first and last 10 minutes metrics
SELECT time_bucket(bucket_interval::INTERVAL, m.time) AS time_bucket, -- Time-bucketed period
SELECT time_bucket(bucket_interval::INTERVAL, m.time) AS time_bucket, -- Time-bucketed period
avg(m.courseovergroundtrue) as courseovergroundtrue,
avg(m.speedoverground) as speedoverground,
avg(m.windspeedapparent) as windspeedapparent,
@@ -527,7 +563,16 @@ BEGIN
COALESCE(avg((m.metrics->'environment.outside.relativeHumidity')::NUMERIC), NULL) as outsidehumidity,
COALESCE(avg((m.metrics->'environment.outside.pressure')::NUMERIC), NULL) as outsidepressure,
COALESCE(avg((m.metrics->'environment.outside.temperature')::NUMERIC), NULL) as outsidetemperature,
COALESCE(avg((m.metrics->'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC), NULL) as stateofcharge,
COALESCE(
NULLIF(
CASE
WHEN (m.metrics->>'electrical.batteries.House.capacity.stateOfCharge') ~ '^-?[0-9]+(\.[0-9]+)?$' THEN
(m.metrics->>'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC
END,
NULL
),
NULL
) as stateofcharge,
COALESCE(avg((m.metrics->'electrical.batteries.House.voltage')::NUMERIC), NULL) as voltage,
ST_MakePoint(last(m.longitude, m.time),last(m.latitude, m.time)) AS geo_point
FROM api.metrics m
@@ -552,13 +597,22 @@ BEGIN
m.status,
COALESCE(metersToKnots((m.metrics->'environment.wind.speedTrue')::NUMERIC), NULL) AS truewindspeed,
COALESCE(radiantToDegrees((m.metrics->'environment.wind.directionTrue')::NUMERIC), NULL) AS truewinddirection,
COALESCE(avg((m.metrics->'environment.water.temperature')::NUMERIC), NULL) as watertemperature,
COALESCE(avg((m.metrics->'environment.depth.belowTransducer')::NUMERIC), NULL) as depth,
COALESCE(avg((m.metrics->'environment.outside.relativeHumidity')::NUMERIC), NULL) as outsidehumidity,
COALESCE(avg((m.metrics->'environment.outside.pressure')::NUMERIC), NULL) as outsidepressure,
COALESCE(avg((m.metrics->'environment.outside.temperature')::NUMERIC), NULL) as outsidetemperature,
COALESCE(avg((m.metrics->'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC), NULL) as stateofcharge,
COALESCE(avg((m.metrics->'electrical.batteries.House.voltage')::NUMERIC), NULL) as voltage,
COALESCE((m.metrics->'environment.water.temperature')::NUMERIC, NULL) as watertemperature,
COALESCE((m.metrics->'environment.depth.belowTransducer')::NUMERIC, NULL) as depth,
COALESCE((m.metrics->'environment.outside.relativeHumidity')::NUMERIC, NULL) as outsidehumidity,
COALESCE((m.metrics->'environment.outside.pressure')::NUMERIC, NULL) as outsidepressure,
COALESCE((m.metrics->'environment.outside.temperature')::NUMERIC, NULL) as outsidetemperature,
COALESCE(
NULLIF(
CASE
WHEN (m.metrics->>'electrical.batteries.House.capacity.stateOfCharge') ~ '^-?[0-9]+(\.[0-9]+)?$' THEN
(m.metrics->>'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC
END,
NULL
),
NULL
) as stateofcharge,
COALESCE((m.metrics->'electrical.batteries.House.voltage')::NUMERIC, NULL) as voltage,
ST_MakePoint(m.longitude, m.latitude) AS geo_point
FROM api.metrics m
WHERE m.latitude IS NOT NULL
@@ -581,13 +635,22 @@ BEGIN
m.status,
COALESCE(metersToKnots((m.metrics->'environment.wind.speedTrue')::NUMERIC), NULL) AS truewindspeed,
COALESCE(radiantToDegrees((m.metrics->'environment.wind.directionTrue')::NUMERIC), NULL) AS truewinddirection,
COALESCE(avg((m.metrics->'environment.water.temperature')::NUMERIC), NULL) as watertemperature,
COALESCE(avg((m.metrics->'environment.depth.belowTransducer')::NUMERIC), NULL) as depth,
COALESCE(avg((m.metrics->'environment.outside.relativeHumidity')::NUMERIC), NULL) as outsidehumidity,
COALESCE(avg((m.metrics->'environment.outside.pressure')::NUMERIC), NULL) as outsidepressure,
COALESCE(avg((m.metrics->'environment.outside.temperature')::NUMERIC), NULL) as outsidetemperature,
COALESCE(avg((m.metrics->'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC), NULL) as stateofcharge,
COALESCE(avg((m.metrics->'electrical.batteries.House.voltage')::NUMERIC), NULL) as voltage,
COALESCE((m.metrics->'environment.water.temperature')::NUMERIC, NULL) as watertemperature,
COALESCE((m.metrics->'environment.depth.belowTransducer')::NUMERIC, NULL) as depth,
COALESCE((m.metrics->'environment.outside.relativeHumidity')::NUMERIC, NULL) as outsidehumidity,
COALESCE((m.metrics->'environment.outside.pressure')::NUMERIC, NULL) as outsidepressure,
COALESCE((m.metrics->'environment.outside.temperature')::NUMERIC, NULL) as outsidetemperature,
COALESCE(
NULLIF(
CASE
WHEN (m.metrics->>'electrical.batteries.House.capacity.stateOfCharge') ~ '^-?[0-9]+(\.[0-9]+)?$' THEN
(m.metrics->>'electrical.batteries.House.capacity.stateOfCharge')::NUMERIC
END,
NULL
),
NULL
) as stateofcharge,
COALESCE((m.metrics->'electrical.batteries.House.voltage')::NUMERIC, NULL) as voltage,
ST_MakePoint(m.longitude, m.latitude) AS geo_point
FROM api.metrics m
WHERE m.latitude IS NOT NULL
@@ -2170,6 +2233,7 @@ AS SELECT id,
_to_moorage_id AS to_moorage_id
FROM api.logbook l
WHERE _to_time IS NOT NULL
AND trip IS NOT NULL
ORDER BY _from_time DESC;
-- Description
COMMENT ON VIEW api.log_view IS 'Log web view';
@@ -2201,6 +2265,7 @@ BEGIN
WHERE id = _id;
END;
$$ LANGUAGE plpgsql;
-- Description
COMMENT ON FUNCTION api.delete_trip_entry_fn IS 'Delete at a specific time a temporal sequence for all trip_* column from a logbook';
-- Update export_logbooks_geojson_point_trips_fn, replace timelapse2_fn, Generate the GeoJSON from the time sequence value
@@ -2239,6 +2304,7 @@ BEGIN
LATERAL jsonb_array_elements(l.log_geojson) AS feature_element; -- Flatten the arrays and create a GeoJSON FeatureCollection
END;
$function$;
-- Description
COMMENT ON FUNCTION api.export_logbooks_geojson_point_trips_fn IS 'Export all selected logs into a geojson `trip` to a geojson as points including properties';
-- Update api role SQL connection to 40
@@ -2268,7 +2334,3 @@ UPDATE public.app_settings
SET value='0.8.1'
WHERE "name"='app.version';
\c postgres
UPDATE cron.job SET username = 'scheduler'; -- Update to scheduler, pending process_queue update
UPDATE cron.job SET username = 'username' WHERE jobname = 'cron_vacuum'; -- Update to superuser for vacuum permissions
UPDATE cron.job SET username = 'username' WHERE jobname = 'job_run_details_cleanup';

View File

@@ -0,0 +1,219 @@
---------------------------------------------------------------------------
-- Copyright 2021-2025 Francois Lacroix <xbgmsharp@gmail.com>
-- This file is part of PostgSail which is released under Apache License, Version 2.0 (the "License").
-- See file LICENSE or go to http://www.apache.org/licenses/LICENSE-2.0 for full license details.
--
-- Migration January-March 2025
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
\echo 'Timing mode is enabled'
\timing
\echo 'Force timezone, just in case'
set timezone to 'UTC';
-- Update metadata table, mark client_id as deprecated
COMMENT ON COLUMN api.metadata.client_id IS 'Deprecated client_id to be removed';
-- Update metrics table, mark client_id as deprecated
COMMENT ON COLUMN api.metrics.client_id IS 'Deprecated client_id to be removed';
-- Update metadata table update configuration column type to jsonb and comment
ALTER TABLE api.metadata ALTER COLUMN "configuration" TYPE jsonb USING "configuration"::jsonb;
COMMENT ON COLUMN api.metadata.configuration IS 'Signalk path mapping for metrics';
-- Update metadata table add new column available_keys and comment
ALTER TABLE api.metadata ADD available_keys jsonb NULL;
COMMENT ON COLUMN api.metadata.available_keys IS 'Signalk paths with unit for custom mapping';
--DROP FUNCTION public.metadata_upsert_trigger_fn();
-- Update metadata_upsert_trigger_fn to metadata table to support configuration and available_keys and deprecated client_id
CREATE OR REPLACE FUNCTION public.metadata_upsert_trigger_fn()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
DECLARE
metadata_id integer;
metadata_active boolean;
BEGIN
-- Require Signalk plugin version 0.4.0
-- Set client_id to new value to allow RLS
--PERFORM set_config('vessel.client_id', NEW.client_id, false);
-- UPSERT - Insert vs Update for Metadata
--RAISE NOTICE 'metadata_upsert_trigger_fn';
--PERFORM set_config('vessel.id', NEW.vessel_id, true);
--RAISE WARNING 'metadata_upsert_trigger_fn [%] [%]', current_setting('vessel.id', true), NEW;
SELECT m.id,m.active INTO metadata_id, metadata_active
FROM api.metadata m
WHERE m.vessel_id IS NOT NULL AND m.vessel_id = current_setting('vessel.id', true);
--RAISE NOTICE 'metadata_id is [%]', metadata_id;
IF metadata_id IS NOT NULL THEN
-- send notification if boat is back online
IF metadata_active is False THEN
-- Add monitor online entry to process queue for later notification
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('monitoring_online', metadata_id, now(), current_setting('vessel.id', true));
END IF;
-- Update vessel metadata
UPDATE api.metadata
SET
name = NEW.name,
mmsi = NEW.mmsi,
--client_id = NEW.client_id,
length = NEW.length,
beam = NEW.beam,
height = NEW.height,
ship_type = NEW.ship_type,
plugin_version = NEW.plugin_version,
signalk_version = NEW.signalk_version,
platform = REGEXP_REPLACE(NEW.platform, '[^a-zA-Z0-9\(\) ]', '', 'g'),
-- configuration = NEW.configuration, -- ignore configuration from vessel, it is manage by user
-- time = NEW.time, ignore the time sent by the vessel as it is out of sync sometimes.
time = NOW(), -- overwrite the time sent by the vessel
available_keys = NEW.available_keys,
active = true
WHERE id = metadata_id;
RETURN NULL; -- Ignore insert
ELSE
IF NEW.vessel_id IS NULL THEN
-- set vessel_id from jwt if not present in INSERT query
NEW.vessel_id := current_setting('vessel.id');
END IF;
-- Ignore and overwrite the time sent by the vessel
NEW.time := NOW();
-- Insert new vessel metadata
RETURN NEW; -- Insert new vessel metadata
END IF;
END;
$function$
;
COMMENT ON FUNCTION public.metadata_upsert_trigger_fn() IS 'process metadata from vessel, upsert';
-- Create or replace the function that will be executed by the trigger
-- Add metadata table trigger for update_metadata_configuration
CREATE OR REPLACE FUNCTION api.update_metadata_configuration()
RETURNS TRIGGER AS $$
BEGIN
-- Require Signalk plugin version 0.4.0
-- Update the configuration field with current date in ISO format
-- Using jsonb_set if configuration is already a JSONB field
IF NEW.configuration IS NOT NULL AND
jsonb_typeof(NEW.configuration) = 'object' THEN
NEW.configuration = jsonb_set(
NEW.configuration,
'{update_at}',
to_jsonb(to_char(NOW(), 'YYYY-MM-DD"T"HH24:MI:SS"Z"'))
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
COMMENT ON FUNCTION api.update_metadata_configuration() IS 'Update the configuration field with current date in ISO format';
-- Create the trigger
CREATE TRIGGER metadata_update_configuration_trigger
BEFORE UPDATE ON api.metadata
FOR EACH ROW
EXECUTE FUNCTION api.update_metadata_configuration();
-- Update api.export_logbook_geojson_linestring_trip_fn, add metadata properties
CREATE OR REPLACE FUNCTION api.export_logbooks_geojson_linestring_trips_fn(
start_log integer DEFAULT NULL::integer,
end_log integer DEFAULT NULL::integer,
start_date text DEFAULT NULL::text,
end_date text DEFAULT NULL::text,
OUT geojson jsonb
) RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
logs_geojson jsonb;
BEGIN
-- Normalize start and end values
IF start_log IS NOT NULL AND end_log IS NULL THEN end_log := start_log; END IF;
IF start_date IS NOT NULL AND end_date IS NULL THEN end_date := start_date; END IF;
WITH logbook_data AS (
-- get the logbook geometry and metadata, an array for each log
SELECT id, name,
starttimestamp(trip),
endtimestamp(trip),
--speed(trip_sog),
duration(trip),
--length(trip) as length, -- Meters
(length(trip) * 0.0005399568)::numeric as distance, -- NM
twavg(trip_sog) as avg_sog,
maxValue(trip_sog) as max_sog,
maxValue(trip_depth) as max_depth, -- Depth
maxValue(trip_batt_charge) as max_batt_charge, -- Battery Charge
maxValue(trip_batt_voltage) as max_batt_voltage, -- Battery Voltage
maxValue(trip_temp_water) as max_temp_water, -- Temperature water
maxValue(trip_temp_out) as max_temp_out, -- Temperature outside
maxValue(trip_pres_out) as max_pres_out, -- Pressure outside
maxValue(trip_hum_out) as max_hum_out, -- Humidity outside
twavg(trip_depth) as avg_depth, -- Depth
twavg(trip_batt_charge) as avg_batt_charge, -- Battery Charge
twavg(trip_batt_voltage) as avg_batt_voltage, -- Battery Voltage
twavg(trip_temp_water) as avg_temp_water, -- Temperature water
twavg(trip_temp_out) as avg_temp_out, -- Temperature outside
twavg(trip_pres_out) as avg_pres_out, -- Pressure outside
twavg(trip_hum_out) as avg_hum_out, -- Humidity outside
trajectory(l.trip)::geometry as track_geog -- extract trip to geography
FROM api.logbook l
WHERE (start_log IS NULL OR l.id >= start_log) AND
(end_log IS NULL OR l.id <= end_log) AND
(start_date IS NULL OR l._from_time >= start_date::TIMESTAMPTZ) AND
(end_date IS NULL OR l._to_time <= end_date::TIMESTAMPTZ + interval '23 hours 59 minutes') AND
l.trip IS NOT NULL
ORDER BY l._from_time ASC
),
collect as (
SELECT ST_Collect(
ARRAY(
SELECT track_geog FROM logbook_data))
)
-- Create the GeoJSON response
SELECT jsonb_build_object(
'type', 'FeatureCollection',
'features', json_agg(ST_AsGeoJSON(logs.*)::json)) INTO geojson FROM logbook_data logs;
END;
$function$;
-- Description
COMMENT ON FUNCTION api.export_logbooks_geojson_linestring_trips_fn IS 'Generate geojson geometry LineString from trip with the corresponding properties';
-- Add public.get_season, return the season based on the input date for logbook tag
CREATE OR REPLACE FUNCTION public.get_season(input_date TIMESTAMPTZ)
RETURNS TEXT AS $$
BEGIN
CASE
WHEN (EXTRACT(MONTH FROM input_date) = 3 AND EXTRACT(DAY FROM input_date) >= 1) OR
(EXTRACT(MONTH FROM input_date) BETWEEN 4 AND 5) THEN
RETURN 'Spring';
WHEN (EXTRACT(MONTH FROM input_date) = 6 AND EXTRACT(DAY FROM input_date) >= 1) OR
(EXTRACT(MONTH FROM input_date) BETWEEN 7 AND 8) THEN
RETURN 'Summer';
WHEN (EXTRACT(MONTH FROM input_date) = 9 AND EXTRACT(DAY FROM input_date) >= 1) OR
(EXTRACT(MONTH FROM input_date) BETWEEN 10 AND 11) THEN
RETURN 'Fall';
ELSE
RETURN 'Winter';
END CASE;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- Refresh permissions
GRANT SELECT ON TABLE api.metrics,api.metadata TO scheduler;
GRANT INSERT, UPDATE, SELECT ON TABLE api.logbook,api.moorages,api.stays TO scheduler;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO scheduler;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO scheduler;
GRANT SELECT, UPDATE ON TABLE public.process_queue TO scheduler;
-- Update version
UPDATE public.app_settings
SET value='0.9.0'
WHERE "name"='app.version';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,734 @@
---------------------------------------------------------------------------
-- Copyright 2021-2025 Francois Lacroix <xbgmsharp@gmail.com>
-- This file is part of PostgSail which is released under Apache License, Version 2.0 (the "License").
-- See file LICENSE or go to http://www.apache.org/licenses/LICENSE-2.0 for full license details.
--
-- Migration June/July 2025
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
\echo 'Timing mode is enabled'
\timing
\echo 'Force timezone, just in case'
set timezone to 'UTC';
-- Update plugin upgrade message
UPDATE public.email_templates
SET email_content='Hello __RECIPIENT__,
Please upgrade your postgsail signalk plugin. Make sure you restart your Signalk instance after upgrading. Be sure to contact me if you encounter any issue.'
WHERE "name"='skplugin_upgrade';
-- DROP FUNCTION api.login(text, text);
-- Update api.login, update the connected_at field to the current time
CREATE OR REPLACE FUNCTION api.login(email text, pass text)
RETURNS auth.jwt_token
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
declare
_role name;
result auth.jwt_token;
app_jwt_secret text;
_email_valid boolean := false;
_email text := email;
_user_id text := null;
_user_disable boolean := false;
headers json := current_setting('request.headers', true)::json;
client_ip text := coalesce(headers->>'x-client-ip', NULL);
begin
-- check email and password
select auth.user_role(email, pass) into _role;
if _role is null then
-- HTTP/403
--raise invalid_password using message = 'invalid user or password';
-- HTTP/401
raise insufficient_privilege using message = 'invalid user or password';
end if;
-- Check if user is disable due to abuse
SELECT preferences['disable'],user_id INTO _user_disable,_user_id
FROM auth.accounts a
WHERE a.email = _email;
IF _user_disable is True then
-- due to the raise, the insert is never committed.
--INSERT INTO process_queue (channel, payload, stored, ref_id)
-- VALUES ('account_disable', _email, now(), _user_id);
RAISE sqlstate 'PT402' using message = 'Account disable, contact us',
detail = 'Quota exceeded',
hint = 'Upgrade your plan';
END IF;
-- Check email_valid and generate OTP
SELECT preferences['email_valid'],user_id INTO _email_valid,_user_id
FROM auth.accounts a
WHERE a.email = _email;
IF _email_valid is null or _email_valid is False THEN
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('email_otp', _email, now(), _user_id);
END IF;
-- Track IP per user to avoid abuse
--RAISE WARNING 'api.login debug: [%],[%]', client_ip, login.email;
IF client_ip IS NOT NULL THEN
UPDATE auth.accounts a SET
preferences = jsonb_recursive_merge(a.preferences, jsonb_build_object('ip', client_ip)),
connected_at = NOW()
WHERE a.email = login.email;
END IF;
-- Get app_jwt_secret
SELECT value INTO app_jwt_secret
FROM app_settings
WHERE name = 'app.jwt_secret';
--RAISE WARNING 'api.login debug: [%],[%],[%]', app_jwt_secret, _role, login.email;
-- Generate jwt
select jwt.sign(
-- row_to_json(r), ''
-- row_to_json(r)::json, current_setting('app.jwt_secret')::text
row_to_json(r)::json, app_jwt_secret
) as token
from (
select _role as role, login.email as email, -- TODO replace with user_id
-- select _role as role, user_id as uid, -- add support in check_jwt
extract(epoch from now())::integer + 60*60 as exp
) r
into result;
return result;
end;
$function$
;
-- Description
COMMENT ON FUNCTION api.login IS 'Handle user login, returns a JWT token with user role and email.';
-- DROP FUNCTION api.monitoring_history_fn(in text, out jsonb);
-- Update monitoring_history_fn to use custom user settings for metrics
CREATE OR REPLACE FUNCTION api.monitoring_history_fn(time_interval text DEFAULT '24'::text, OUT history_metrics jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $function$
DECLARE
bucket_interval interval := '5 minutes';
BEGIN
RAISE NOTICE '-> monitoring_history_fn';
SELECT CASE time_interval
WHEN '24' THEN '5 minutes'
WHEN '48' THEN '2 hours'
WHEN '72' THEN '4 hours'
WHEN '168' THEN '7 hours'
ELSE '5 minutes'
END bucket INTO bucket_interval;
RAISE NOTICE '-> monitoring_history_fn % %', time_interval, bucket_interval;
WITH history_table AS (
SELECT time_bucket(bucket_interval::INTERVAL, mt.time) AS time_bucket,
avg(-- Water Temperature
COALESCE(
mt.metrics->'water'->>'temperature',
mt.metrics->>(md.configuration->>'waterTemperatureKey'),
mt.metrics->>'environment.water.temperature'
)::FLOAT) AS waterTemperature,
avg(-- Inside Temperature
COALESCE(
mt.metrics->'temperature'->>'inside',
mt.metrics->>(md.configuration->>'insideTemperatureKey'),
mt.metrics->>'environment.inside.temperature'
)::FLOAT) AS insideTemperature,
avg(-- Outside Temperature
COALESCE(
mt.metrics->'temperature'->>'outside',
mt.metrics->>(md.configuration->>'outsideTemperatureKey'),
mt.metrics->>'environment.outside.temperature'
)::FLOAT) AS outsideTemperature,
avg(-- Wind Speed True
COALESCE(
mt.metrics->'wind'->>'speed',
mt.metrics->>(md.configuration->>'windSpeedKey'),
mt.metrics->>'environment.wind.speedTrue'
)::FLOAT) AS windSpeedOverGround,
avg(-- Inside Humidity
COALESCE(
mt.metrics->'humidity'->>'inside',
mt.metrics->>(md.configuration->>'insideHumidityKey'),
mt.metrics->>'environment.inside.relativeHumidity',
mt.metrics->>'environment.inside.humidity'
)::FLOAT) AS insideHumidity,
avg(-- Outside Humidity
COALESCE(
mt.metrics->'humidity'->>'outside',
mt.metrics->>(md.configuration->>'outsideHumidityKey'),
mt.metrics->>'environment.outside.relativeHumidity',
mt.metrics->>'environment.outside.humidity'
)::FLOAT) AS outsideHumidity,
avg(-- Outside Pressure
COALESCE(
mt.metrics->'pressure'->>'outside',
mt.metrics->>(md.configuration->>'outsidePressureKey'),
mt.metrics->>'environment.outside.pressure'
)::FLOAT) AS outsidePressure,
avg(--Inside Pressure
COALESCE(
mt.metrics->'pressure'->>'inside',
mt.metrics->>(md.configuration->>'insidePressureKey'),
mt.metrics->>'environment.inside.pressure'
)::FLOAT) AS insidePressure,
avg(-- Battery Charge (State of Charge)
COALESCE(
mt.metrics->'battery'->>'charge',
mt.metrics->>(md.configuration->>'stateOfChargeKey'),
mt.metrics->>'electrical.batteries.House.capacity.stateOfCharge'
)::FLOAT) AS batteryCharge,
avg(-- Battery Voltage
COALESCE(
mt.metrics->'battery'->>'voltage',
mt.metrics->>(md.configuration->>'voltageKey'),
mt.metrics->>'electrical.batteries.House.voltage'
)::FLOAT) AS batteryVoltage,
avg(-- Water Depth
COALESCE(
mt.metrics->'water'->>'depth',
mt.metrics->>(md.configuration->>'depthKey'),
mt.metrics->>'environment.depth.belowTransducer'
)::FLOAT) AS depth
FROM api.metrics mt
JOIN api.metadata md ON md.vessel_id = mt.vessel_id
WHERE mt.time > (NOW() AT TIME ZONE 'UTC' - INTERVAL '1 hours' * time_interval::NUMERIC)
GROUP BY time_bucket
ORDER BY time_bucket asc
)
SELECT jsonb_agg(history_table) INTO history_metrics FROM history_table;
END
$function$
;
-- Description
COMMENT ON FUNCTION api.monitoring_history_fn(in text, out jsonb) IS 'Export metrics from a time period 24h, 48h, 72h, 7d';
-- DROP FUNCTION public.cron_alerts_fn();
-- Update cron_alerts_fn to check for alerts, filters out empty strings (""), so they are not included in the result.
CREATE OR REPLACE FUNCTION public.cron_alerts_fn()
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
alert_rec record;
default_last_metric TIMESTAMPTZ := NOW() - interval '1 day';
last_metric TIMESTAMPTZ;
metric_rec record;
app_settings JSONB;
user_settings JSONB;
alerting JSONB;
_alarms JSONB;
alarms TEXT;
alert_default JSONB := '{
"low_pressure_threshold": 990,
"high_wind_speed_threshold": 30,
"low_water_depth_threshold": 1,
"min_notification_interval": 6,
"high_pressure_drop_threshold": 12,
"low_battery_charge_threshold": 90,
"low_battery_voltage_threshold": 12.5,
"low_water_temperature_threshold": 10,
"low_indoor_temperature_threshold": 7,
"low_outdoor_temperature_threshold": 3
}';
BEGIN
-- Check for new event notification pending update
RAISE NOTICE 'cron_alerts_fn';
FOR alert_rec in
SELECT
a.user_id,a.email,v.vessel_id,
COALESCE((a.preferences->'alert_last_metric')::TEXT, default_last_metric::TEXT) as last_metric,
(alert_default || ( -- Filters out empty strings (""), so they are not included in the result.
SELECT jsonb_object_agg(key, value)
FROM jsonb_each(a.preferences->'alerting')
WHERE value <> '""'
)) as alerting,
(a.preferences->'alarms')::JSONB as alarms,
m.configuration as config
FROM auth.accounts a
LEFT JOIN auth.vessels AS v ON v.owner_email = a.email
LEFT JOIN api.metadata AS m ON m.vessel_id = v.vessel_id
WHERE (a.preferences->'alerting'->'enabled')::boolean = True
AND m.active = True
LOOP
RAISE NOTICE '-> cron_alerts_fn for [%]', alert_rec;
PERFORM set_config('vessel.id', alert_rec.vessel_id, false);
PERFORM set_config('user.email', alert_rec.email, false);
--RAISE WARNING 'public.cron_process_alert_rec_fn() scheduler vessel.id %, user.id', current_setting('vessel.id', false), current_setting('user.id', false);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(alert_rec.vessel_id::TEXT);
RAISE NOTICE '-> cron_alerts_fn checking user_settings [%]', user_settings;
-- Get all metrics from the last last_metric avg by 5 minutes
FOR metric_rec in
SELECT time_bucket('5 minutes', m.time) AS time_bucket,
avg(-- Inside Temperature
COALESCE(
mt.metrics->'temperature'->>'inside',
mt.metrics->>(md.configuration->>'insideTemperatureKey'),
mt.metrics->>'environment.inside.temperature'
)::FLOAT) AS intemp,
avg(-- Wind Speed True
COALESCE(
mt.metrics->'wind'->>'speed',
mt.metrics->>(md.configuration->>'windSpeedKey'),
mt.metrics->>'environment.wind.speedTrue'
)::FLOAT) AS wind,
avg(-- Water Depth
COALESCE(
mt.metrics->'water'->>'depth',
mt.metrics->>(md.configuration->>'depthKey'),
mt.metrics->>'environment.depth.belowTransducer'
)::FLOAT) AS watdepth,
avg(-- Outside Temperature
COALESCE(
m.metrics->'temperature'->>'outside',
m.metrics->>(alert_rec.config->>'outsideTemperatureKey'),
m.metrics->>'environment.outside.temperature'
)::NUMERIC) AS outtemp,
avg(-- Water Temperature
COALESCE(
m.metrics->'water'->>'temperature',
m.metrics->>(alert_rec.config->>'waterTemperatureKey'),
m.metrics->>'environment.water.temperature'
)::NUMERIC) AS wattemp,
avg(-- Outside Pressure
COALESCE(
m.metrics->'pressure'->>'outside',
m.metrics->>(alert_rec.config->>'outsidePressureKey'),
m.metrics->>'environment.outside.pressure'
)::NUMERIC) AS pressure,
avg(-- Battery Voltage
COALESCE(
m.metrics->'battery'->>'voltage',
m.metrics->>(alert_rec.config->>'voltageKey'),
m.metrics->>'electrical.batteries.House.voltage'
)::NUMERIC) AS voltage,
avg(-- Battery Charge (State of Charge)
COALESCE(
m.metrics->'battery'->>'charge',
m.metrics->>(alert_rec.config->>'stateOfChargeKey'),
m.metrics->>'electrical.batteries.House.capacity.stateOfCharge'
)::NUMERIC) AS charge
FROM api.metrics m
WHERE vessel_id = alert_rec.vessel_id
AND m.time >= alert_rec.last_metric::TIMESTAMPTZ
GROUP BY time_bucket
ORDER BY time_bucket ASC LIMIT 100
LOOP
RAISE NOTICE '-> cron_alerts_fn checking metrics [%]', metric_rec;
RAISE NOTICE '-> cron_alerts_fn checking alerting [%]', alert_rec.alerting;
--RAISE NOTICE '-> cron_alerts_fn checking debug [%] [%]', kelvinToCel(metric_rec.intemp), (alert_rec.alerting->'low_indoor_temperature_threshold');
IF metric_rec.intemp IS NOT NULL AND public.kelvintocel(metric_rec.intemp::NUMERIC) < (alert_rec.alerting->'low_indoor_temperature_threshold')::NUMERIC then
RAISE NOTICE '-> cron_alerts_fn checking debug indoor_temp [%]', (alert_rec.alarms->'low_indoor_temperature_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug indoor_temp [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_indoor_temperature_threshold'->>'date') IS NULL) OR
(((_alarms->'low_indoor_temperature_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_indoor_temperature_threshold": {"value": '|| kelvinToCel(metric_rec.intemp) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_outdoor_temperature_threshold value:'|| kelvinToCel(metric_rec.intemp) ||' date:'|| metric_rec.time_bucket ||' "}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_indoor_temperature_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_indoor_temperature_threshold';
END IF;
IF metric_rec.outtemp IS NOT NULL AND public.kelvintocel(metric_rec.outtemp::NUMERIC) < (alert_rec.alerting->>'low_outdoor_temperature_threshold')::NUMERIC then
RAISE NOTICE '-> cron_alerts_fn checking debug outdoor_temp [%]', (alert_rec.alarms->'low_outdoor_temperature_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug outdoor_temp [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_outdoor_temperature_threshold'->>'date') IS NULL) OR
(((_alarms->'low_outdoor_temperature_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_outdoor_temperature_threshold": {"value": '|| kelvinToCel(metric_rec.outtemp) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_outdoor_temperature_threshold value:'|| kelvinToCel(metric_rec.outtemp) ||' date:'|| metric_rec.time_bucket ||' "}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_outdoor_temperature_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_outdoor_temperature_threshold';
END IF;
IF metric_rec.wattemp IS NOT NULL AND public.kelvintocel(metric_rec.wattemp::NUMERIC) < (alert_rec.alerting->>'low_water_temperature_threshold')::NUMERIC then
RAISE NOTICE '-> cron_alerts_fn checking debug water_temp [%]', (alert_rec.alarms->'low_water_temperature_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug water_temp [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_water_temperature_threshold'->>'date') IS NULL) OR
(((_alarms->'low_water_temperature_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_water_temperature_threshold": {"value": '|| kelvinToCel(metric_rec.wattemp) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_water_temperature_threshold value:'|| kelvinToCel(metric_rec.wattemp) ||' date:'|| metric_rec.time_bucket ||' "}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_temperature_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_temperature_threshold';
END IF;
IF metric_rec.watdepth IS NOT NULL AND metric_rec.watdepth::NUMERIC < (alert_rec.alerting->'low_water_depth_threshold')::NUMERIC then
RAISE NOTICE '-> cron_alerts_fn checking debug water_depth [%]', (alert_rec.alarms->'low_water_depth_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug water_depth [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_water_depth_threshold'->>'date') IS NULL) OR
(((_alarms->'low_water_depth_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_water_depth_threshold": {"value": '|| metric_rec.watdepth ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_water_depth_threshold value:'|| ROUND(metric_rec.watdepth,2) ||' date:'|| metric_rec.time_bucket ||' "}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_depth_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_water_depth_threshold';
END IF;
if metric_rec.pressure IS NOT NULL AND metric_rec.pressure::NUMERIC < (alert_rec.alerting->'high_pressure_drop_threshold')::NUMERIC then
RAISE NOTICE '-> cron_alerts_fn checking debug pressure [%]', (alert_rec.alarms->'high_pressure_drop_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug pressure [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'high_pressure_drop_threshold'->>'date') IS NULL) OR
(((_alarms->'high_pressure_drop_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"high_pressure_drop_threshold": {"value": '|| metric_rec.pressure ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "high_pressure_drop_threshold value:'|| ROUND(metric_rec.pressure,2) ||' date:'|| metric_rec.time_bucket ||' "}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug high_pressure_drop_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug high_pressure_drop_threshold';
END IF;
IF metric_rec.wind IS NOT NULL AND metric_rec.wind::NUMERIC > (alert_rec.alerting->'high_wind_speed_threshold')::NUMERIC then
RAISE NOTICE '-> cron_alerts_fn checking debug wind [%]', (alert_rec.alarms->'high_wind_speed_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug wind [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'high_wind_speed_threshold'->>'date') IS NULL) OR
(((_alarms->'high_wind_speed_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"high_wind_speed_threshold": {"value": '|| metric_rec.wind ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "high_wind_speed_threshold value:'|| ROUND(metric_rec.wind,2) ||' date:'|| metric_rec.time_bucket ||' "}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug high_wind_speed_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug high_wind_speed_threshold';
END IF;
IF metric_rec.voltage IS NOT NULL AND metric_rec.voltage::NUMERIC < (alert_rec.alerting->'low_battery_voltage_threshold')::NUMERIC then
RAISE NOTICE '-> cron_alerts_fn checking debug voltage [%]', (alert_rec.alarms->'low_battery_voltage_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug voltage [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_battery_voltage_threshold'->>'date') IS NULL) OR
(((_alarms->'low_battery_voltage_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_battery_voltage_threshold": {"value": '|| metric_rec.voltage ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_battery_voltage_threshold value:'|| ROUND(metric_rec.voltage,2) ||' date:'|| metric_rec.time_bucket ||' "}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_voltage_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_voltage_threshold';
END IF;
IF metric_rec.charge IS NOT NULL AND (metric_rec.charge::NUMERIC*100) < (alert_rec.alerting->'low_battery_charge_threshold')::NUMERIC then
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', (alert_rec.alarms->'low_battery_charge_threshold'->>'date')::TIMESTAMPTZ;
RAISE NOTICE '-> cron_alerts_fn checking debug [%]', metric_rec.time_bucket::TIMESTAMPTZ;
-- Get latest alarms
SELECT preferences->'alarms' INTO _alarms FROM auth.accounts a WHERE a.email = current_setting('user.email', false);
-- Is alarm in the min_notification_interval time frame
IF (
((_alarms->'low_battery_charge_threshold'->>'date') IS NULL) OR
(((_alarms->'low_battery_charge_threshold'->>'date')::TIMESTAMPTZ
+ ((interval '1 hour') * (alert_rec.alerting->>'min_notification_interval')::NUMERIC))
< metric_rec.time_bucket::TIMESTAMPTZ)
) THEN
-- Add alarm
alarms := '{"low_battery_charge_threshold": {"value": '|| (metric_rec.charge*100) ||', "date":"' || metric_rec.time_bucket || '"}}';
-- Merge alarms
SELECT public.jsonb_recursive_merge(_alarms::jsonb, alarms::jsonb) into _alarms;
-- Update alarms for user
PERFORM api.update_user_preferences_fn('{alarms}'::TEXT, _alarms::TEXT);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(current_setting('vessel.id', false));
SELECT user_settings::JSONB || ('{"alert": "low_battery_charge_threshold value:'|| ROUND(metric_rec.charge::NUMERIC*100,2) ||' date:'|| metric_rec.time_bucket ||' "}'::text)::JSONB into user_settings;
-- Send notification
PERFORM send_notification_fn('alert'::TEXT, user_settings::JSONB);
-- DEBUG
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_charge_threshold +interval';
END IF;
RAISE NOTICE '-> cron_alerts_fn checking debug low_battery_charge_threshold';
END IF;
-- Record last metrics time
SELECT metric_rec.time_bucket INTO last_metric;
END LOOP;
PERFORM api.update_user_preferences_fn('{alert_last_metric}'::TEXT, last_metric::TEXT);
END LOOP;
END;
$function$
;
-- Description
COMMENT ON FUNCTION public.cron_alerts_fn() IS 'init by pg_cron to check for alerts';
-- DROP FUNCTION public.process_pre_logbook_fn(int4);
-- Update process_pre_logbook_fn to detect and avoid logbook we more than 1000NM in less 15h
CREATE OR REPLACE FUNCTION public.process_pre_logbook_fn(_id integer)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
logbook_rec record;
avg_rec record;
geo_rec record;
_invalid_time boolean;
_invalid_interval boolean;
_invalid_distance boolean;
_invalid_ratio boolean;
count_metric numeric;
previous_stays_id numeric;
current_stays_departed text;
current_stays_id numeric;
current_stays_active boolean;
timebucket boolean;
BEGIN
-- If _id is not NULL
IF _id IS NULL OR _id < 1 THEN
RAISE WARNING '-> process_pre_logbook_fn invalid input %', _id;
RETURN;
END IF;
-- Get the logbook record with all necessary fields exist
SELECT * INTO logbook_rec
FROM api.logbook
WHERE active IS false
AND id = _id
AND _from_lng IS NOT NULL
AND _from_lat IS NOT NULL
AND _to_lng IS NOT NULL
AND _to_lat IS NOT NULL;
-- Ensure the query is successful
IF logbook_rec.vessel_id IS NULL THEN
RAISE WARNING '-> process_pre_logbook_fn invalid logbook %', _id;
RETURN;
END IF;
PERFORM set_config('vessel.id', logbook_rec.vessel_id, false);
--RAISE WARNING 'public.process_logbook_queue_fn() scheduler vessel.id %, user.id', current_setting('vessel.id', false), current_setting('user.id', false);
-- Check if all metrics are within 50meters base on geo loc
count_metric := logbook_metrics_dwithin_fn(logbook_rec._from_time::TEXT, logbook_rec._to_time::TEXT, logbook_rec._from_lng::NUMERIC, logbook_rec._from_lat::NUMERIC);
RAISE NOTICE '-> process_pre_logbook_fn logbook_metrics_dwithin_fn count:[%]', count_metric;
-- Calculate logbook data average and geo
-- Update logbook entry with the latest metric data and calculate data
avg_rec := logbook_update_avg_fn(logbook_rec.id, logbook_rec._from_time::TEXT, logbook_rec._to_time::TEXT);
geo_rec := logbook_update_geom_distance_fn(logbook_rec.id, logbook_rec._from_time::TEXT, logbook_rec._to_time::TEXT);
-- Avoid/ignore/delete logbook stationary movement or time sync issue
-- Check time start vs end
SELECT logbook_rec._to_time::TIMESTAMPTZ < logbook_rec._from_time::TIMESTAMPTZ INTO _invalid_time;
-- Is distance is less than 0.010
SELECT geo_rec._track_distance < 0.010 INTO _invalid_distance;
-- Is duration is less than 100sec
SELECT (logbook_rec._to_time::TIMESTAMPTZ - logbook_rec._from_time::TIMESTAMPTZ) < (100::text||' secs')::interval INTO _invalid_interval;
-- If we have more than 800NM in less 15h
IF geo_rec._track_distance >= 800 AND (logbook_rec._to_time::TIMESTAMPTZ - logbook_rec._from_time::TIMESTAMPTZ) < (15::text||' hours')::interval THEN
_invalid_distance := True;
_invalid_interval := True;
--RAISE NOTICE '-> process_pre_logbook_fn invalid logbook data id [%], _invalid_distance [%], _invalid_interval [%]', logbook_rec.id, _invalid_distance, _invalid_interval;
END IF;
-- If we have less than 20 metrics or less than 0.5NM or less than avg 0.5knts
-- Is within metrics represent more or equal than 60% of the total entry
IF count_metric::NUMERIC <= 20 OR geo_rec._track_distance < 0.5 OR avg_rec.avg_speed < 0.5 THEN
SELECT (count_metric::NUMERIC / avg_rec.count_metric::NUMERIC) >= 0.60 INTO _invalid_ratio;
END IF;
-- if stationary fix data metrics,logbook,stays,moorage
IF _invalid_time IS True OR _invalid_distance IS True
OR _invalid_interval IS True OR count_metric = avg_rec.count_metric
OR _invalid_ratio IS True
OR avg_rec.count_metric <= 3 THEN
RAISE NOTICE '-> process_pre_logbook_fn invalid logbook data id [%], _invalid_time [%], _invalid_distance [%], _invalid_interval [%], count_metric_in_zone [%], count_metric_log [%], _invalid_ratio [%]',
logbook_rec.id, _invalid_time, _invalid_distance, _invalid_interval, count_metric, avg_rec.count_metric, _invalid_ratio;
-- Update metrics status to moored
UPDATE api.metrics
SET status = 'moored'
WHERE time >= logbook_rec._from_time::TIMESTAMPTZ
AND time <= logbook_rec._to_time::TIMESTAMPTZ
AND vessel_id = current_setting('vessel.id', false);
-- Update logbook
UPDATE api.logbook
SET notes = 'invalid logbook data, stationary need to fix metrics?'
WHERE vessel_id = current_setting('vessel.id', false)
AND id = logbook_rec.id;
-- Get related stays
SELECT id,departed,active INTO current_stays_id,current_stays_departed,current_stays_active
FROM api.stays s
WHERE s.vessel_id = current_setting('vessel.id', false)
AND s.arrived = logbook_rec._to_time::TIMESTAMPTZ;
-- Update related stays
UPDATE api.stays s
SET notes = 'invalid stays data, stationary need to fix metrics?'
WHERE vessel_id = current_setting('vessel.id', false)
AND arrived = logbook_rec._to_time::TIMESTAMPTZ;
-- Find previous stays
SELECT id INTO previous_stays_id
FROM api.stays s
WHERE s.vessel_id = current_setting('vessel.id', false)
AND s.arrived < logbook_rec._to_time::TIMESTAMPTZ
ORDER BY s.arrived DESC LIMIT 1;
-- Update previous stays with the departed time from current stays
-- and set the active state from current stays
UPDATE api.stays
SET departed = current_stays_departed::TIMESTAMPTZ,
active = current_stays_active
WHERE vessel_id = current_setting('vessel.id', false)
AND id = previous_stays_id;
-- Clean up, remove invalid logbook and stay entry
DELETE FROM api.logbook WHERE id = logbook_rec.id;
RAISE WARNING '-> process_pre_logbook_fn delete invalid logbook [%]', logbook_rec.id;
DELETE FROM api.stays WHERE id = current_stays_id;
RAISE WARNING '-> process_pre_logbook_fn delete invalid stays [%]', current_stays_id;
RETURN;
END IF;
--IF (logbook_rec.notes IS NULL) THEN -- run one time only
-- -- If duration is over 24h or number of entry is over 400, check for stays and potential multiple logs with stationary location
-- IF (logbook_rec._to_time::TIMESTAMPTZ - logbook_rec._from_time::TIMESTAMPTZ) > INTERVAL '24 hours'
-- OR avg_rec.count_metric > 400 THEN
-- timebucket := public.logbook_metrics_timebucket_fn('15 minutes'::TEXT, logbook_rec.id, logbook_rec._from_time::TIMESTAMPTZ, logbook_rec._to_time::TIMESTAMPTZ);
-- -- If true exit current process as the current logbook need to be re-process.
-- IF timebucket IS True THEN
-- RETURN;
-- END IF;
-- ELSE
-- timebucket := public.logbook_metrics_timebucket_fn('5 minutes'::TEXT, logbook_rec.id, logbook_rec._from_time::TIMESTAMPTZ, logbook_rec._to_time::TIMESTAMPTZ);
-- -- If true exit current process as the current logbook need to be re-process.
-- IF timebucket IS True THEN
-- RETURN;
-- END IF;
-- END IF;
--END IF;
-- Add logbook entry to process queue for later processing
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('new_logbook', logbook_rec.id, NOW(), current_setting('vessel.id', true));
END;
$function$
;
COMMENT ON FUNCTION public.process_pre_logbook_fn(int4) IS 'Detect/Avoid/ignore/delete logbook stationary movement or time sync issue';
-- Revoke security definer
--ALTER FUNCTION api.update_logbook_observations_fn(_id integer, observations text) SECURITY INVOKER;
--ALTER FUNCTION api.delete_logbook_fn(_id integer) SECURITY INVOKER;
ALTER FUNCTION api.merge_logbook_fn(_id integer, _id integer) SECURITY INVOKER;
GRANT DELETE ON TABLE public.process_queue TO user_role;
GRANT SELECT ON ALL TABLES IN SCHEMA api TO user_role;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api TO user_role;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO user_role;
GRANT UPDATE (status) ON api.metrics TO user_role;
GRANT UPDATE ON api.logbook TO user_role;
DROP POLICY IF EXISTS api_user_role ON api.metrics;
CREATE POLICY api_user_role ON api.metrics TO user_role
USING (vessel_id = current_setting('vessel.id', false))
WITH CHECK (vessel_id = current_setting('vessel.id', false));
-- Update version
UPDATE public.app_settings
SET value='0.9.3'
WHERE "name"='app.version';
--\c postgres
--UPDATE cron.job SET username = 'scheduler'; -- Update to scheduler
--UPDATE cron.job SET username = current_user WHERE jobname = 'cron_vacuum'; -- Update to superuser for vacuum permissions
--UPDATE cron.job SET username = current_user WHERE jobname = 'job_run_details_cleanup';

View File

@@ -0,0 +1,578 @@
---------------------------------------------------------------------------
-- Copyright 2021-2025 Francois Lacroix <xbgmsharp@gmail.com>
-- This file is part of PostgSail which is released under Apache License, Version 2.0 (the "License").
-- See file LICENSE or go to http://www.apache.org/licenses/LICENSE-2.0 for full license details.
--
-- Migration August 2025
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
\echo 'Timing mode is enabled'
\timing
\echo 'Force timezone, just in case'
set timezone to 'UTC';
-- Lint fix
CREATE INDEX ON api.stays_ext (vessel_id);
ALTER TABLE api.stays_ext FORCE ROW LEVEL SECURITY;
ALTER TABLE api.metadata_ext FORCE ROW LEVEL SECURITY;
ALTER TABLE api.metadata ADD PRIMARY KEY (vessel_id);
COMMENT ON CONSTRAINT metadata_vessel_id_fkey ON api.metadata IS 'Link api.metadata with auth.vessels via vessel_id using FOREIGN KEY and REFERENCES';
COMMENT ON CONSTRAINT metrics_vessel_id_fkey ON api.metrics IS 'Link api.metrics api.metadata via vessel_id using FOREIGN KEY and REFERENCES';
COMMENT ON CONSTRAINT logbook_vessel_id_fkey ON api.logbook IS 'Link api.stays with api.metadata via vessel_id using FOREIGN KEY and REFERENCES';
COMMENT ON CONSTRAINT moorages_vessel_id_fkey ON api.moorages IS 'Link api.stays with api.metadata via vessel_id using FOREIGN KEY and REFERENCES';
COMMENT ON CONSTRAINT stays_vessel_id_fkey ON api.stays IS 'Link api.stays with api.metadata via vessel_id using FOREIGN KEY and REFERENCES';
COMMENT ON COLUMN api.logbook._from IS 'Name of the location where the log started, usually a moorage name';
COMMENT ON COLUMN api.logbook._to IS 'Name of the location where the log ended, usually a moorage name';
COMMENT ON COLUMN api.logbook.vessel_id IS 'Unique identifier for the vessel associated with the api.metadata entry';
COMMENT ON COLUMN api.metrics.vessel_id IS 'Unique identifier for the vessel associated with the api.metadata entry';
COMMENT ON COLUMN api.moorages.vessel_id IS 'Unique identifier for the vessel associated with the api.metadata entry';
COMMENT ON COLUMN api.moorages.nominatim IS 'Output of the nominatim reverse geocoding service, see https://nominatim.org/release-docs/develop/api/Reverse/';
COMMENT ON COLUMN api.moorages.overpass IS 'Output of the overpass API, see https://wiki.openstreetmap.org/wiki/Overpass_API';
COMMENT ON COLUMN api.stays.vessel_id IS 'Unique identifier for the vessel associated with the api.metadata entry';
COMMENT ON COLUMN api.stays_ext.vessel_id IS 'Unique identifier for the vessel associated with the api.metadata entry';
COMMENT ON COLUMN api.metadata_ext.vessel_id IS 'Unique identifier for the vessel associated with the api.metadata entry';
COMMENT ON COLUMN api.metadata.mmsi IS 'Maritime Mobile Service Identity (MMSI) number associated with the vessel, link to public.mid';
COMMENT ON COLUMN api.metadata.ship_type IS 'Type of ship associated with the vessel, link to public.aistypes';
COMMENT ON TRIGGER ts_insert_blocker ON api.metrics IS 'manage by timescaledb, prevent direct insert on hypertable api.metrics';
COMMENT ON TRIGGER ensure_vessel_role_exists ON auth.vessels IS 'ensure vessel role exists';
COMMENT ON TRIGGER encrypt_pass ON auth.accounts IS 'execute function auth.encrypt_pass()';
-- Fix typo in comment
COMMENT ON FUNCTION public.new_account_entry_fn() IS 'trigger process_queue on INSERT for new account';
-- Update missing comment on trigger
COMMENT ON TRIGGER encrypt_pass ON auth.accounts IS 'execute function auth.encrypt_pass()';
-- Update new account email subject
UPDATE public.email_templates
SET email_subject='Welcome aboard!',
email_content='Welcome aboard __RECIPIENT__,
Congratulations!
You successfully created an account.
Keep in mind to register your vessel.
Happy sailing!'
WHERE "name"='new_account';
-- Update deactivated email subject
UPDATE public.email_templates
SET email_subject='We hate to see you go'
WHERE "name"='deactivated';
-- Update first badge message
UPDATE public.badges
SET description='Nice work logging your first sail! Youre officially a helmsman now!
While youre at it, why not spread the word about Postgsail? ⭐
If you found it useful, consider starring the project on GitHub, contributing, or even sponsoring the project to help steer it forward.
Happy sailing! 🌊
https://github.com/xbgmsharp/postgsail
https://github.com/sponsors/xbgmsharp/'
WHERE "name"='Helmsman';
-- DROP FUNCTION public.stays_delete_trigger_fn();
-- Add public.stay_delete_trigger_fn trigger function to delete stays_ext and process_queue entries
CREATE OR REPLACE FUNCTION public.stay_delete_trigger_fn()
RETURNS trigger
LANGUAGE plpgsql
AS $function$
BEGIN
RAISE NOTICE 'stay_delete_trigger_fn [%]', OLD;
-- If api.stays is deleted, deleted entry in api.stays_ext table as well.
IF EXISTS (SELECT FROM information_schema.tables
WHERE table_schema = 'api'
AND table_name = 'stays_ext') THEN
-- Delete stays_ext
DELETE FROM api.stays_ext s
WHERE s.stay_id = OLD.id;
END IF;
-- Delete process_queue references
DELETE FROM public.process_queue p
WHERE p.payload = OLD.id::TEXT
AND p.ref_id = OLD.vessel_id
AND p.channel LIKE '%_stays';
RETURN OLD;
END;
$function$
;
-- Description
COMMENT ON FUNCTION public.stay_delete_trigger_fn() IS 'When stays is delete, stays_ext need to deleted as well.';
-- Create trigger to delete stays_ext and process_queue entries
create trigger stay_delete_trigger before
delete
on
api.stays for each row execute function stay_delete_trigger_fn();
COMMENT ON TRIGGER stay_delete_trigger ON api.stays IS 'BEFORE DELETE ON api.stays run function public.stay_delete_trigger_fn to delete reference and stay_ext need to deleted.';
-- Remove trigger that duplicate the OTP validation entry on insert for new account, it is handle by api.login
DROP TRIGGER new_account_otp_validation_entry ON auth.accounts;
-- DEBUG
DROP TRIGGER IF EXISTS debug_trigger ON public.process_queue;
DROP FUNCTION IF EXISTS debug_trigger_fn;
CREATE FUNCTION debug_trigger_fn() RETURNS trigger AS $debug$
DECLARE
BEGIN
--RAISE NOTICE 'debug_trigger_fn [%]', NEW;
IF NEW.channel = 'email_otp' THEN
RAISE WARNING 'debug_trigger_fn: channel is email_otp [%]', NEW;
END IF;
RETURN NEW;
END;
$debug$ LANGUAGE plpgsql;
CREATE TRIGGER debug_trigger AFTER INSERT ON public.process_queue
FOR EACH ROW EXECUTE FUNCTION debug_trigger_fn();
-- Description
COMMENT ON TRIGGER debug_trigger ON public.process_queue IS 'Log debug information.';
DROP TRIGGER debug_trigger ON public.process_queue;
-- DROP FUNCTION api.login(text, text);
-- Update api.login function to handle user disable and email verification, update error code with invalid_email_or_password
CREATE OR REPLACE FUNCTION api.login(email text, pass text)
RETURNS auth.jwt_token
LANGUAGE plpgsql
SECURITY DEFINER
AS $function$
declare
_role name;
result auth.jwt_token;
app_jwt_secret text;
_email_valid boolean := false;
_email text := email;
_user_id text := null;
_user_disable boolean := false;
headers json := current_setting('request.headers', true)::json;
client_ip text := coalesce(headers->>'x-client-ip', NULL);
begin
-- check email and password
select auth.user_role(email, pass) into _role;
if _role is null then
-- HTTP/403
--raise invalid_password using message = 'invalid user or password';
-- HTTP/401
--raise insufficient_privilege using message = 'invalid user or password';
-- HTTP/402 - to distinguish with JWT Expiration token
RAISE sqlstate 'PT402' using message = 'invalid email or password',
detail = 'invalid auth specification',
hint = 'Use a valid email and password';
end if;
-- Gather user information
SELECT preferences['disable'], preferences['email_valid'], user_id
INTO _user_disable,_email_valid,_user_id
FROM auth.accounts a
WHERE a.email = _email;
-- Check if user is disable due to abuse
IF _user_disable::BOOLEAN IS TRUE THEN
-- due to the raise, the insert is never committed.
--INSERT INTO process_queue (channel, payload, stored, ref_id)
-- VALUES ('account_disable', _email, now(), _user_id);
RAISE sqlstate 'PT402' using message = 'Account disable, contact us',
detail = 'Quota exceeded',
hint = 'Upgrade your plan';
END IF;
-- Check if email has been verified, if not generate OTP
IF _email_valid::BOOLEAN IS NOT True THEN
INSERT INTO process_queue (channel, payload, stored, ref_id)
VALUES ('email_otp', _email, now(), _user_id);
END IF;
-- Track IP per user to avoid abuse
--RAISE WARNING 'api.login debug: [%],[%]', client_ip, login.email;
IF client_ip IS NOT NULL THEN
UPDATE auth.accounts a SET
preferences = jsonb_recursive_merge(a.preferences, jsonb_build_object('ip', client_ip)),
connected_at = NOW()
WHERE a.email = login.email;
END IF;
-- Get app_jwt_secret
SELECT value INTO app_jwt_secret
FROM app_settings
WHERE name = 'app.jwt_secret';
--RAISE WARNING 'api.login debug: [%],[%],[%]', app_jwt_secret, _role, login.email;
-- Generate jwt
select jwt.sign(
-- row_to_json(r), ''
-- row_to_json(r)::json, current_setting('app.jwt_secret')::text
row_to_json(r)::json, app_jwt_secret
) as token
from (
select _role as role, login.email as email, -- TODO replace with user_id
-- select _role as role, user_id as uid, -- add support in check_jwt
extract(epoch from now())::integer + 60*60 as exp
) r
into result;
return result;
end;
$function$
;
-- Description
COMMENT ON FUNCTION api.login(text, text) IS 'Handle user login, returns a JWT token with user role and email.';
-- DROP FUNCTION public.cron_windy_fn();
-- Update cron_windy_fn to support custom user metrics
CREATE OR REPLACE FUNCTION public.cron_windy_fn()
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
windy_rec record;
default_last_metric TIMESTAMPTZ := NOW() - interval '1 day';
last_metric TIMESTAMPTZ := NOW();
metric_rec record;
windy_metric jsonb;
app_settings jsonb;
user_settings jsonb;
windy_pws jsonb;
BEGIN
-- Check for new observations pending update
RAISE NOTICE 'cron_process_windy_fn';
-- Gather url from app settings
app_settings := get_app_settings_fn();
-- Find users with Windy active and with an active vessel
-- Map account id to Windy Station ID
FOR windy_rec in
SELECT
a.id,a.email,v.vessel_id,v.name,
COALESCE((a.preferences->'windy_last_metric')::TEXT, default_last_metric::TEXT) as last_metric
FROM auth.accounts a
LEFT JOIN auth.vessels AS v ON v.owner_email = a.email
LEFT JOIN api.metadata AS m ON m.vessel_id = v.vessel_id
WHERE (a.preferences->'public_windy')::boolean = True
AND m.active = True
LOOP
RAISE NOTICE '-> cron_process_windy_fn for [%]', windy_rec;
PERFORM set_config('vessel.id', windy_rec.vessel_id, false);
--RAISE WARNING 'public.cron_process_windy_rec_fn() scheduler vessel.id %, user.id', current_setting('vessel.id', false), current_setting('user.id', false);
-- Gather user settings
user_settings := get_user_settings_from_vesselid_fn(windy_rec.vessel_id::TEXT);
RAISE NOTICE '-> cron_process_windy_fn checking user_settings [%]', user_settings;
-- Get all metrics from the last windy_last_metric avg by 5 minutes
-- TODO json_agg to send all data in once, but issue with py jsonb transformation decimal.
FOR metric_rec in
SELECT time_bucket('5 minutes', mt.time) AS time_bucket,
avg(-- Outside Temperature
COALESCE(
mt.metrics->'temperature'->>'outside',
mt.metrics->>(md.configuration->>'outsideTemperatureKey'),
mt.metrics->>'environment.outside.temperature'
)::FLOAT) AS temperature,
avg(-- Outside Pressure
COALESCE(
mt.metrics->'pressure'->>'outside',
mt.metrics->>(md.configuration->>'outsidePressureKey'),
mt.metrics->>'environment.outside.pressure'
)::FLOAT) AS pressure,
avg(-- Outside Humidity
COALESCE(
mt.metrics->'humidity'->>'outside',
mt.metrics->>(md.configuration->>'outsideHumidityKey'),
mt.metrics->>'environment.outside.relativeHumidity',
mt.metrics->>'environment.outside.humidity'
)::FLOAT) AS rh,
avg(-- Wind Direction True
COALESCE(
mt.metrics->'wind'->>'direction',
mt.metrics->>(md.configuration->>'windDirectionKey'),
mt.metrics->>'environment.wind.directionTrue'
)::FLOAT) AS winddir,
avg(-- Wind Speed True
COALESCE(
mt.metrics->'wind'->>'speed',
mt.metrics->>(md.configuration->>'windSpeedKey'),
mt.metrics->>'environment.wind.speedTrue',
mt.metrics->>'environment.wind.speedApparent'
)::FLOAT) AS wind,
max(-- Max Wind Speed True
COALESCE(
mt.metrics->'wind'->>'speed',
mt.metrics->>(md.configuration->>'windSpeedKey'),
mt.metrics->>'environment.wind.speedTrue',
mt.metrics->>'environment.wind.speedApparent'
)::FLOAT) AS gust,
last(latitude, mt.time) AS lat,
last(longitude, mt.time) AS lng
FROM api.metrics mt
JOIN api.metadata md ON md.vessel_id = mt.vessel_id
WHERE md.vessel_id = windy_rec.vessel_id
AND mt.time >= windy_rec.last_metric::TIMESTAMPTZ
GROUP BY time_bucket
ORDER BY time_bucket ASC LIMIT 100
LOOP
RAISE NOTICE '-> cron_process_windy_fn checking metrics [%]', metric_rec;
if metric_rec.wind is null or metric_rec.temperature is null
or metric_rec.pressure is null or metric_rec.rh is null then
-- Ignore when there is no metrics.
-- Send notification
PERFORM send_notification_fn('windy_error'::TEXT, user_settings::JSONB);
-- Disable windy
PERFORM api.update_user_preferences_fn('{public_windy}'::TEXT, 'false'::TEXT);
RETURN;
end if;
-- https://community.windy.com/topic/8168/report-your-weather-station-data-to-windy
-- temp from kelvin to celcuis
-- winddir from radiant to degres
-- rh from ratio to percentage
SELECT jsonb_build_object(
'dateutc', metric_rec.time_bucket,
'station', windy_rec.id,
'name', windy_rec.name,
'lat', metric_rec.lat,
'lon', metric_rec.lng,
'wind', metric_rec.wind,
'gust', metric_rec.gust,
'pressure', metric_rec.pressure,
'winddir', radiantToDegrees(metric_rec.winddir::numeric),
'temp', kelvinToCel(metric_rec.temperature::numeric),
'rh', valToPercent(metric_rec.rh::numeric)
) INTO windy_metric;
RAISE NOTICE '-> cron_process_windy_fn checking windy_metrics [%]', windy_metric;
SELECT windy_pws_py_fn(windy_metric, user_settings, app_settings) into windy_pws;
RAISE NOTICE '-> cron_process_windy_fn Windy PWS [%]', ((windy_pws->'header')::JSONB ? 'id');
IF NOT((user_settings->'settings')::JSONB ? 'windy') and ((windy_pws->'header')::JSONB ? 'id') then
RAISE NOTICE '-> cron_process_windy_fn new Windy PWS [%]', (windy_pws->'header')::JSONB->>'id';
-- Send metrics to Windy
PERFORM api.update_user_preferences_fn('{windy}'::TEXT, ((windy_pws->'header')::JSONB->>'id')::TEXT);
-- Send notification
PERFORM send_notification_fn('windy'::TEXT, user_settings::JSONB);
-- Refresh user settings after first success
user_settings := get_user_settings_from_vesselid_fn(windy_rec.vessel_id::TEXT);
END IF;
-- Record last metrics time
SELECT metric_rec.time_bucket INTO last_metric;
END LOOP;
PERFORM api.update_user_preferences_fn('{windy_last_metric}'::TEXT, last_metric::TEXT);
END LOOP;
END;
$function$
;
-- Description
COMMENT ON FUNCTION public.cron_windy_fn() IS 'init by pg_cron to create (or update) station and uploading observations to Windy Personal Weather Station observations';
-- DROP FUNCTION api.merge_logbook_fn(int4, int4);
-- Update merge_logbook_fn to handle more metrics and limit moorage deletion
CREATE OR REPLACE FUNCTION api.merge_logbook_fn(id_start integer, id_end integer)
RETURNS void
LANGUAGE plpgsql
AS $function$
DECLARE
logbook_rec_start record;
logbook_rec_end record;
log_name text;
avg_rec record;
geo_rec record;
geojson jsonb;
extra_json jsonb;
t_rec record;
BEGIN
-- If id_start or id_end is not NULL
IF (id_start IS NULL OR id_start < 1) OR (id_end IS NULL OR id_end < 1) THEN
RAISE WARNING '-> merge_logbook_fn invalid input % %', id_start, id_end;
RETURN;
END IF;
-- If id_end is lower than id_start
IF id_end <= id_start THEN
RAISE WARNING '-> merge_logbook_fn invalid input % < %', id_end, id_start;
RETURN;
END IF;
-- Get the start logbook record with all necessary fields exist
SELECT * INTO logbook_rec_start
FROM api.logbook
WHERE active IS false
AND id = id_start
AND _from_lng IS NOT NULL
AND _from_lat IS NOT NULL
AND _to_lng IS NOT NULL
AND _to_lat IS NOT NULL;
-- Ensure the query is successful
IF logbook_rec_start.vessel_id IS NULL THEN
RAISE WARNING '-> merge_logbook_fn invalid logbook %', id_start;
RETURN;
END IF;
-- Get the end logbook record with all necessary fields exist
SELECT * INTO logbook_rec_end
FROM api.logbook
WHERE active IS false
AND id = id_end
AND _from_lng IS NOT NULL
AND _from_lat IS NOT NULL
AND _to_lng IS NOT NULL
AND _to_lat IS NOT NULL;
-- Ensure the query is successful
IF logbook_rec_end.vessel_id IS NULL THEN
RAISE WARNING '-> merge_logbook_fn invalid logbook %', id_end;
RETURN;
END IF;
RAISE WARNING '-> merge_logbook_fn logbook start:% end:%', id_start, id_end;
PERFORM set_config('vessel.id', logbook_rec_start.vessel_id, false);
-- Calculate logbook data average and geo
-- Update logbook entry with the latest metric data and calculate data
avg_rec := logbook_update_avg_fn(logbook_rec_start.id, logbook_rec_start._from_time::TEXT, logbook_rec_end._to_time::TEXT);
geo_rec := logbook_update_geom_distance_fn(logbook_rec_start.id, logbook_rec_start._from_time::TEXT, logbook_rec_end._to_time::TEXT);
-- Process `propulsion.*.runTime` and `navigation.log`
-- Calculate extra json
extra_json := logbook_update_extra_json_fn(logbook_rec_start.id, logbook_rec_start._from_time::TEXT, logbook_rec_end._to_time::TEXT);
-- add the avg_wind_speed
extra_json := extra_json || jsonb_build_object('avg_wind_speed', avg_rec.avg_wind_speed);
-- generate logbook name, concat _from_location and _to_location from moorage name
SELECT CONCAT(logbook_rec_start._from, ' to ', logbook_rec_end._to) INTO log_name;
-- mobilitydb, add spaciotemporal sequence
-- reduce the numbers of metrics by skipping row or aggregate time-series
-- By default the signalk PostgSail plugin report one entry every minute.
IF avg_rec.count_metric < 30 THEN -- if less ~20min trip we keep it all data
t_rec := public.logbook_update_metrics_short_fn(avg_rec.count_metric, logbook_rec_start._from_time, logbook_rec_end._to_time);
ELSIF avg_rec.count_metric < 2000 THEN -- if less ~33h trip we skip data
t_rec := public.logbook_update_metrics_fn(avg_rec.count_metric, logbook_rec_start._from_time, logbook_rec_end._to_time);
ELSE -- As we have too many data, we time-series aggregate data
t_rec := public.logbook_update_metrics_timebucket_fn(avg_rec.count_metric, logbook_rec_start._from_time, logbook_rec_end._to_time);
END IF;
--RAISE NOTICE 'mobilitydb [%]', t_rec;
IF t_rec.trajectory IS NULL THEN
RAISE WARNING '-> process_logbook_queue_fn, vessel_id [%], invalid mobilitydb data [%] [%]', logbook_rec_start.vessel_id, logbook_rec_start.id, t_rec;
RETURN;
END IF;
RAISE NOTICE 'Updating valid logbook entry logbook id:[%] start:[%] end:[%]', logbook_rec_start.id, logbook_rec_start._from_time, logbook_rec_end._to_time;
UPDATE api.logbook
SET
duration = (logbook_rec_end._to_time::TIMESTAMPTZ - logbook_rec_start._from_time::TIMESTAMPTZ),
avg_speed = avg_rec.avg_speed,
max_speed = avg_rec.max_speed,
max_wind_speed = avg_rec.max_wind_speed,
-- Set _to metrics from end logbook
_to = logbook_rec_end._to,
_to_moorage_id = logbook_rec_end._to_moorage_id,
_to_lat = logbook_rec_end._to_lat,
_to_lng = logbook_rec_end._to_lng,
_to_time = logbook_rec_end._to_time,
name = log_name,
distance = geo_rec._track_distance,
extra = extra_json,
notes = NULL, -- reset pre_log process
trip = t_rec.trajectory,
trip_cog = t_rec.courseovergroundtrue,
trip_sog = t_rec.speedoverground,
trip_twa = t_rec.windspeedapparent,
trip_tws = t_rec.truewindspeed,
trip_twd = t_rec.truewinddirection,
trip_notes = t_rec.notes,
trip_status = t_rec.status,
trip_depth = t_rec.depth,
trip_batt_charge = t_rec.stateofcharge,
trip_batt_voltage = t_rec.voltage,
trip_temp_water = t_rec.watertemperature,
trip_temp_out = t_rec.outsidetemperature,
trip_pres_out = t_rec.outsidepressure,
trip_hum_out = t_rec.outsidehumidity,
trip_tank_level = t_rec.tankLevel,
trip_solar_voltage = t_rec.solarVoltage,
trip_solar_power = t_rec.solarPower,
trip_heading = t_rec.heading
WHERE id = logbook_rec_start.id;
/*** Deprecated removed column
-- GeoJSON require track_geom field geometry linestring
--geojson := logbook_update_geojson_fn(logbook_rec.id, logbook_rec._from_time::TEXT, logbook_rec._to_time::TEXT);
-- GeoJSON require trip* columns
geojson := api.logbook_update_geojson_trip_fn(logbook_rec_start.id);
UPDATE api.logbook
SET -- Update the data column, it should be generate dynamically on request
-- However there is a lot of dependencies to consider for a larger cleanup
-- badges, qgis etc... depends on track_geom
-- many export and others functions depends on track_geojson
track_geojson = geojson,
track_geog = trajectory(t_rec.trajectory),
track_geom = trajectory(t_rec.trajectory)::geometry
-- embedding = NULL,
-- spatial_embedding = NULL
WHERE id = logbook_rec_start.id;
-- GeoJSON Timelapse require track_geojson geometry point
-- Add properties to the geojson for timelapse purpose
PERFORM public.logbook_timelapse_geojson_fn(logbook_rec_start.id);
***/
-- Update logbook mark for deletion
UPDATE api.logbook
SET notes = 'mark for deletion'
WHERE id = logbook_rec_end.id;
-- Update related stays mark for deletion
UPDATE api.stays
SET notes = 'mark for deletion'
WHERE arrived = logbook_rec_start._to_time;
-- Update related moorages mark for deletion
-- We can't delete the stays and moorages as it might expand to other previous logs and stays
--UPDATE api.moorages
-- SET notes = 'mark for deletion'
-- WHERE id = logbook_rec_start._to_moorage_id;
-- Clean up, remove invalid logbook and stay, moorage entry
DELETE FROM api.logbook WHERE id = logbook_rec_end.id;
RAISE WARNING '-> merge_logbook_fn delete logbook id [%]', logbook_rec_end.id;
DELETE FROM api.stays WHERE arrived = logbook_rec_start._to_time;
RAISE WARNING '-> merge_logbook_fn delete stay arrived [%]', logbook_rec_start._to_time;
-- We can't delete the stays and moorages as it might expand to other previous logs and stays
-- Delete the moorage only if exactly one record exists with that id.
DELETE FROM api.moorages
WHERE id = logbook_rec_start._to_moorage_id
AND (
SELECT COUNT(*)
FROM api.logbook
WHERE _from_moorage_id = logbook_rec_start._to_moorage_id
OR _to_moorage_id = logbook_rec_start._to_moorage_id
) = 1;
RAISE WARNING '-> merge_logbook_fn delete moorage id [%]', logbook_rec_start._to_moorage_id;
END;
$function$
;
-- Description
COMMENT ON FUNCTION api.merge_logbook_fn(int4, int4) IS 'Merge 2 logbook by id, from the start of the lower log id and the end of the higher log id, update the calculate data as well (avg, geojson)';
-- Add api.counts_fn to count logbook, moorages and stays entries
CREATE OR REPLACE FUNCTION api.counts_fn()
RETURNS jsonb
LANGUAGE sql
AS $function$
SELECT jsonb_build_object(
'logs', (SELECT COUNT(*) FROM api.logbook),
'moorages', (SELECT COUNT(*) FROM api.moorages),
'stays', (SELECT COUNT(*) FROM api.stays)
);
$function$;
-- Description
COMMENT ON FUNCTION api.counts_fn() IS 'count logbook, moorages and stays entries';
-- allow user_role to delete on api.stays_ext
GRANT DELETE ON TABLE api.stays_ext TO user_role;
-- refresh permissions
GRANT SELECT ON ALL TABLES IN SCHEMA api TO user_role;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api TO user_role;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO user_role;
-- Update version
UPDATE public.app_settings
SET value='0.9.4'
WHERE "name"='app.version';
\c postgres
-- Add cron job for vacuum and cleanup the public tables
INSERT INTO cron.job (schedule,command,nodename,nodeport,database,username,active,jobname)
VALUES ('1 1 * * 0','VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) public.process_queue,public.app_settings,public.email_templates;','/var/run/postgresql/',5432,'signalk','username',false,'cron_vacuum_public');
--UPDATE cron.job SET username = 'scheduler'; -- Update to scheduler
--UPDATE cron.job SET username = current_user WHERE jobname = 'cron_vacuum'; -- Update to superuser for vacuum permissions
--UPDATE cron.job SET username = current_user WHERE jobname = 'job_run_details_cleanup';

View File

@@ -1 +1 @@
0.8.1
0.9.4

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,5 @@
FROM node:lts
#FROM node:lts
FROM mcr.microsoft.com/devcontainers/javascript-node:22
ENV DEBIAN_FRONTEND=noninteractive
# Install and update the system

View File

@@ -1,5 +1,5 @@
# PostgSail Unit Tests
The Unit Tests allow to automatically validate api workflow.
The Unit Tests allow to automatically validate SQL and API workflow.
## A global overview
Based on `mocha` & `psql`

View File

@@ -27,6 +27,7 @@ const metrics_aava = require('./metrics_sample_aava.json');
const fs = require('fs');
let configtime = new Date().toISOString();
// CNAMEs Array
[
@@ -39,7 +40,7 @@ const fs = require('fs');
vessel_metadata: {
name: "kapla",
mmsi: "123456789",
client_id: "vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd",
//client_id: "vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd",
length: "12",
beam: "10",
height: "24",
@@ -59,8 +60,8 @@ const fs = require('fs');
user_views: [
// not processed yet, { url: '/stays_view', res_body_length: 1},
// not processed yet, { url: '/moorages_view', res_body_length: 1},
{ url: '/logs_view', res_body_length: 0},
{ url: '/log_view', res_body_length: 2},
{ url: '/logs_view', res_body_length: 0}, // not processed yet so empty
{ url: '/log_view', res_body_length: 0}, // not processed yet so empty
//{ url: '/stats_view', res_body_length: 1},
{ url: '/vessels_view', res_body_length: 1},
],
@@ -177,6 +178,18 @@ const fs = require('fs');
obj_name: 'settings'
}
}
],
config_fn: [
{ url: '/metadata?select=configuration',
res: {
obj_name: 'configuration'
}
},
{ url: `/metadata?select=configuration&configuration->>update_at=gt.${configtime}`,
res: {
obj_name: 'settings'
}
},
]
},
{ cname: process.env.PGSAIL_API_URI, name: "PostgSail unit test, aava",
@@ -186,8 +199,8 @@ const fs = require('fs');
preferences: { key: '{email_notifications}', value: false }, /* Disable email_notifications */
vessel_metadata: {
name: "aava",
mmsi: "787654321",
client_id: "vessels.urn:mrn:imo:mmsi:787654321",
mmsi: "n/a",
//client_id: "vessels.urn:mrn:imo:mmsi:787654321",
length: "12",
beam: "10",
height: "24",
@@ -206,8 +219,8 @@ const fs = require('fs');
user_views: [
// not processed yet, { url: '/stays_view', res_body_length: 1},
// not processed yet, { url: '/moorages_view', res_body_length: 1},
{ url: '/logs_view', res_body_length: 0},
{ url: '/log_view', res_body_length: 1},
{ url: '/logs_view', res_body_length: 0}, // not processed yet so empty
{ url: '/log_view', res_body_length: 0}, // not processed yet so empty
//{ url: '/stats_view', res_body_length: 1},
{ url: '/vessels_view', res_body_length: 1},
],
@@ -318,6 +331,30 @@ const fs = require('fs');
obj_name: 'settings'
}
},
],
config_fn: [
{ url: '/metadata?select=configuration',
res: {
obj_name: 'configuration'
}
},
{ url: `/metadata?select=configuration&configuration->>update_at=gt.${configtime}`,
res: {
obj_name: 'settings'
}
},
],
meta_ext_fn: [
{ url: '/metadata_ext?',
res: {
obj_name: 'configuration'
}
},
{ url: `/metadata_ext?`,
res: {
obj_name: 'image'
}
},
]
}
].forEach( function(test){
@@ -596,7 +633,7 @@ request.set('User-Agent', 'PostgSail unit tests');
.set('Authorization', `Bearer ${vessel_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set('Prefer', 'return=headers-only,resolution=merge-duplicates')
.set('Prefer', 'missing=default,return=headers-only,resolution=merge-duplicates')
.end(function(err,res){
res.status.should.equal(201);
//console.log(res.header);
@@ -611,14 +648,15 @@ request.set('User-Agent', 'PostgSail unit tests');
describe("Vessel POST metrics, JWT vessel_role", function(){
let data = [];
//console.log(vessel_metrics['metrics'][0]);
//console.log(test.vessel_metrics['metrics'][0]);
let i;
for (i = 0; i < test.vessel_metrics['metrics'].length; i++) {
data[i] = test.vessel_metrics['metrics'][i];
// Override time, -2h to allow to new data later without delay.
data[i]['time'] = moment.utc().subtract(1, 'day').add(i, 'minutes').format();
// Override client_id
data[i]['client_id'] = test.vessel_metadata.client_id;
//data[i]['client_id'] = test.vessel_metadata.client_id;
data[i]['client_id'] = null;
}
// The last entry are invalid and should be ignore.
// - Invalid status
@@ -843,6 +881,37 @@ request.set('User-Agent', 'PostgSail unit tests');
}); // Function OTP endpoint
*/
describe("Function Metadata configuration endpoint, JWT vessel_role", function(){
let otp = null;
test.config_fn.forEach(function (subtest) {
it(`${subtest.url}`, function(done) {
try {
//console.log(`${subtest.url} ${subtest.res}`);
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
.get(subtest.url)
.set('Authorization', `Bearer ${vessel_jwt}`)
.set('Accept', 'application/json')
.end(function(err,res){
res.status.should.equal(200);
should.exist(res.header['content-type']);
should.exist(res.header['server']);
res.header['content-type'].should.match(new RegExp('json','g'));
res.header['server'].should.match(new RegExp('postgrest','g'));
console.log(res.body);
should.exist(res.body);
done(err);
});
}
catch (error) {
done();
}
});
});
}); // Function metadata configuration endpoint
}); // OpenAPI description
}); // CNAMEs Array

View File

@@ -28,14 +28,15 @@ const metrics_simulator = require('./metrics_sample_simulator.json');
vessel_metadata: {
name: "aava",
mmsi: "787654321",
client_id: "vessels.urn:mrn:imo:mmsi:787654321",
//client_id: "vessels.urn:mrn:imo:mmsi:787654321",
length: "12",
beam: "10",
height: "24",
ship_type: "37",
plugin_version: "1.0.2",
signalk_version: "1.20.0",
time: moment().subtract(69, 'minutes').format()
time: moment().subtract(69, 'minutes').format(),
available_keys: [],
},
vessel_metrics: metrics_simulator,
user_tables: [
@@ -48,7 +49,7 @@ const metrics_simulator = require('./metrics_sample_simulator.json');
// not processed yet, { url: '/stays_view', res_body_length: 1},
// not processed yet, { url: '/moorages_view', res_body_length: 1},
{ url: '/logs_view', res_body_length: 1},
{ url: '/log_view', res_body_length: 2},
{ url: '/log_view', res_body_length: 0}, // not processed yet so empty
//{ url: '/stats_view', res_body_length: 1},
{ url: '/vessels_view', res_body_length: 1},
],
@@ -412,18 +413,19 @@ request.set('User-Agent', 'PostgSail unit tests');
describe("Vessel POST metadata, JWT vessel_role", function(){
it('/metadata', function(done) {
it('/metadata?on_conflict=vessel_id', function(done) {
request = supertest.agent(test.cname);
request
.post('/metadata')
.post('/metadata?on_conflict=vessel_id')
.send(test.vessel_metadata)
.set('Authorization', `Bearer ${vessel_jwt}`)
.set('Accept', 'application/json')
.set('Content-Type', 'application/json')
.set('Prefer', 'return=headers-only')
.set('Prefer', 'missing=default,return=headers-only,resolution=merge-duplicates')
.end(function(err,res){
res.status.should.equal(201);
//console.log(res.body);
//console.log(res.header);
res.status.should.equal(200);
should.exist(res.header['server']);
res.header['server'].should.match(new RegExp('postgrest','g'));
done(err);
@@ -442,7 +444,8 @@ request.set('User-Agent', 'PostgSail unit tests');
// Override time, +1h because previous sample include 47 entry.
data[i]['time'] = moment.utc().subtract(2, 'hours').add(i, 'minutes').format();
// Override client_id
data[i]['client_id'] = test.vessel_metadata.client_id;
//data[i]['client_id'] = test.vessel_metadata.client_id;
data[i]['client_id'] = null;
}
//console.log(data[0]);

View File

@@ -31,7 +31,7 @@ var moment = require('moment');
vessel_metadata: {
name: "kapla",
mmsi: "123456789",
client_id: "vessels.urn:mrn:imo:mmsi:123456789",
//client_id: "vessels.urn:mrn:imo:mmsi:123456789",
length: "12",
beam: "10",
height: "24",
@@ -249,7 +249,7 @@ var moment = require('moment');
vessel_metadata: {
name: "aava",
mmsi: "787654321",
client_id: "vessels.urn:mrn:imo:mmsi:787654321",
//client_id: "vessels.urn:mrn:imo:mmsi:787654321",
length: "12",
beam: "10",
height: "24",

View File

@@ -163,6 +163,10 @@ var moment = require("moment");
url: "/rpc/update_user_preferences_fn",
payload: { key: "{public_monitoring}", value: true },
},
{
url: "/rpc/update_user_preferences_fn",
payload: { key: "{public_timelapse}", value: true },
},
],
},
{
@@ -684,8 +688,8 @@ var moment = require("moment");
should.exist(res.body);
let event = res.body;
//console.log(event);
// minimum events log for kapla & aava 13 + 4 email_otp = 17
event.length.should.be.aboveOrEqual(13);
// minimum events log per users 6 + 4 logs + OTP one per login
event.length.should.be.aboveOrEqual(11);
done(err);
});
});

View File

@@ -151,7 +151,7 @@ var moment = require("moment");
request.set("User-Agent", "PostgSail unit tests");
describe("With no JWT as api_anonymous", function () {
it("/logs_view, api_anonymous no jwt token", function (done) {
it("/logs_view, api_anonymous no jwt token, x-is-public header", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
@@ -167,7 +167,7 @@ var moment = require("moment");
done(err);
});
});
it("/log_view, api_anonymous no jwt token", function (done) {
it("/log_view, api_anonymous no jwt token, x-is-public header", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
@@ -183,7 +183,7 @@ var moment = require("moment");
done(err);
});
});
it("/monitoring_view, api_anonymous no jwt token", function (done) {
it("/monitoring_view, api_anonymous no jwt token, x-is-public header", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
@@ -200,7 +200,7 @@ var moment = require("moment");
done(err);
});
});
it("/rpc/export_logbooks_geojson_linestring_trips_fn, api_anonymous no jwt token", function (done) {
it("/rpc/export_logbooks_geojson_linestring_trips_fn, api_anonymous no jwt token, x-is-public header", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
@@ -214,10 +214,18 @@ var moment = require("moment");
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
should.exist(res.body.geojson);
/*
if (res.body.geojson.features == null) { // aava
//res.body.geojson.features.should.not.be.ok();
done(err);
}
res.body.geojson.features.length.should.be.equal(4);
*/
done(err);
});
});
it("/rpc/export_logbooks_geojson_point_trips_fn, api_anonymous no jwt token", function (done) {
it("/rpc/export_logbooks_geojson_point_trips_fn, api_anonymous no jwt token, x-is-public header", function (done) {
// Reset agent so we do not save cookies
request = supertest.agent(test.cname);
request
@@ -231,6 +239,14 @@ var moment = require("moment");
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
should.exist(res.body.geojson);
/*
if (res.body.geojson.features == null) { // aava
//res.body.geojson.features.should.not.be.ok();
done(err);
}
res.body.geojson.features.length.should.be.equal(53);
*/
done(err);
});
});

View File

@@ -183,6 +183,7 @@ var moment = require("moment");
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
should.exist(res.body.geojson);
done(err);
});
});
@@ -193,12 +194,13 @@ var moment = require("moment");
.post(test.replay_full.url)
.set("Accept", "application/json")
.end(function (err, res) {
console.log(res.text);
console.log(res.body);
res.status.should.equal(200);
should.exist(res.header["content-type"]);
should.exist(res.header["server"]);
res.header["content-type"].should.match(new RegExp("json", "g"));
res.header["server"].should.match(new RegExp("postgrest", "g"));
should.exist(res.body.geojson);
done(err);
});
});

View File

@@ -8,8 +8,5 @@
"moment": "^2.29.4",
"should": "^13.2.3",
"supertest": "^6.3.3"
},
"devDependencies": {
"schemalint": "^2.0.5"
}
}

View File

@@ -18,3 +18,10 @@ SELECT api.ispublic_fn('kapla', 'public_logs', 1);
SELECT api.ispublic_fn('kapla', 'public_logs', 3);
SELECT api.ispublic_fn('kapla', 'public_monitoring');
SELECT api.ispublic_fn('kapla', 'public_timelapse');
SELECT api.ispublic_fn('aava', 'public_test');
SELECT api.ispublic_fn('aava', 'public_logs_list');
SELECT api.ispublic_fn('aava', 'public_logs', 1);
SELECT api.ispublic_fn('aava', 'public_logs', 3);
SELECT api.ispublic_fn('aava', 'public_monitoring');
SELECT api.ispublic_fn('aava', 'public_timelapse');

View File

@@ -21,6 +21,24 @@ ispublic_fn | f
-[ RECORD 1 ]--
ispublic_fn | t
-[ RECORD 1 ]--
ispublic_fn | t
-[ RECORD 1 ]--
ispublic_fn | f
-[ RECORD 1 ]--
ispublic_fn | f
-[ RECORD 1 ]--
ispublic_fn | f
-[ RECORD 1 ]--
ispublic_fn | t
-[ RECORD 1 ]--
ispublic_fn | t
-[ RECORD 1 ]--
ispublic_fn | f

View File

@@ -21,6 +21,9 @@ SELECT v.vessel_id as "vessel_id" FROM auth.vessels v WHERE v.owner_email = 'dem
--\echo :"vessel_id"
SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
-- user_role
SET ROLE user_role;
-- Test logbook for user
\echo 'logbook'
SELECT count(*) FROM api.logbook WHERE vessel_id = current_setting('vessel.id', false);
@@ -38,7 +41,7 @@ SELECT active,name IS NOT NULL AS name,geog,stay_code FROM api.stays WHERE vesse
\echo 'eventlogs_view'
SELECT count(*) from api.eventlogs_view;
-- Test event logs view for user
-- Test stats logs view for user
\echo 'stats_logs_fn'
SELECT api.stats_logs_fn(null, null) INTO stats_jsonb;
SELECT stats_logs_fn->'name' AS name,

View File

@@ -11,6 +11,7 @@ user_id | t
-[ RECORD 1 ]
vessel_id | t
SET
logbook
-[ RECORD 1 ]
count | 2
@@ -70,19 +71,19 @@ count | 11
stats_logs_fn
SELECT 1
-[ RECORD 1 ]+----------
-[ RECORD 1 ]+--------
name | "kapla"
count | 4
max_speed | 9.5
max_distance | 68.8677
max_duration | "PT1H11M"
?column? | 3
?column? | 90.6030
?column? | "PT2H44M"
?column? | 44.2
?column? | 3
?column? | 4
?column? | 4
count | 2
max_speed | 6.5
max_distance | 8.8968
max_duration | "PT27M"
?column? | 2
?column? | 16.5415
?column? | "PT47M"
?column? | 37.2
?column? | 2
?column? | 1
?column? | 2
first_date | t
last_date | t

View File

@@ -15,7 +15,12 @@ select current_database();
\echo 'Check the number of process pending'
-- Should be 24
SELECT count(*) as jobs from public.process_queue pq where pq.processed is null;
--set role scheduler
-- Switch to the scheduler role
--\echo 'Switch to the scheduler role'
--SET ROLE scheduler;
-- Should be 24
SELECT count(*) as jobs from public.process_queue pq where pq.processed is null;
-- Run the cron jobs
SELECT public.run_cron_jobs();
-- Check any pending job
SELECT count(*) as any_pending_jobs from public.process_queue pq where pq.processed is null;

View File

@@ -9,6 +9,9 @@ Check the number of process pending
-[ RECORD 1 ]
jobs | 24
-[ RECORD 1 ]
jobs | 24
-[ RECORD 1 ]-+-
run_cron_jobs |

View File

@@ -20,7 +20,7 @@ SELECT current_user, current_setting('user.email', true), current_setting('vesse
--SELECT a.pass,v.name,m.client_id FROM auth.accounts a JOIN auth.vessels v ON a.email = 'demo+kapla@openplotter.cloud' AND a.role = 'user_role' AND cast(a.preferences->>'email_valid' as Boolean) = True AND v.owner_email = a.email JOIN api.metadata m ON m.vessel_id = v.vessel_id;
--SELECT a.pass,v.name,m.client_id FROM auth.accounts a JOIN auth.vessels v ON a.email = 'demo+kapla@openplotter.cloud' AND a.role = 'user_role' AND v.owner_email = a.email JOIN api.metadata m ON m.vessel_id = v.vessel_id;
\echo 'link vessel and user based on current_setting'
SELECT v.name,m.client_id FROM auth.accounts a JOIN auth.vessels v ON a.role = 'user_role' AND v.owner_email = a.email JOIN api.metadata m ON m.vessel_id = v.vessel_id ORDER BY a.id DESC;
SELECT v.name, m.vessel_id IS NOT NULL AS vessel_id FROM auth.accounts a JOIN auth.vessels v ON a.role = 'user_role' AND v.owner_email = a.email JOIN api.metadata m ON m.vessel_id = v.vessel_id ORDER BY a.id DESC;
\echo 'auth.accounts details'
SELECT a.user_id IS NOT NULL AS user_id, a.email, a.first, a.last, a.pass IS NOT NULL AS pass, a.role, a.preferences->'telegram'->'chat' AS telegram, a.preferences->'pushover_user_key' AS pushover_user_key FROM auth.accounts AS a ORDER BY a.id DESC;
@@ -29,7 +29,7 @@ SELECT a.user_id IS NOT NULL AS user_id, a.email, a.first, a.last, a.pass IS NOT
SELECT v.vessel_id IS NOT NULL AS vessel_id, v.owner_email, v.mmsi, v.name, v.role FROM auth.vessels AS v;
\echo 'api.metadata details'
--
SELECT m.id, m.name, m.mmsi, m.client_id, m.length, m.beam, m.height, m.ship_type, m.plugin_version, m.signalk_version, m.time IS NOT NULL AS time, m.active FROM api.metadata AS m;
SELECT vessel_id IS NOT NULL AS vessel_id_not_null, m.name, m.mmsi, m.length, m.beam, m.height, m.ship_type, m.plugin_version, m.signalk_version, m.time IS NOT NULL AS time, m.active, configuration IS NOT NULL AS configuration_not_null, available_keys FROM api.metadata AS m ORDER BY m.name DESC;
--
-- grafana
@@ -48,14 +48,14 @@ SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
--SELECT current_user, current_setting('user.email', true), current_setting('vessel.client_id', true), current_setting('vessel.id', true);
SELECT current_user, current_setting('user.email', true);
SELECT v.name AS __text, m.client_id AS __value FROM auth.vessels v JOIN api.metadata m ON v.owner_email = 'demo+kapla@openplotter.cloud' and m.vessel_id = v.vessel_id;
SELECT v.name AS __text, m.vessel_id IS NOT NULL AS __value FROM auth.vessels v JOIN api.metadata m ON v.owner_email = 'demo+kapla@openplotter.cloud' and m.vessel_id = v.vessel_id;
\echo 'auth.vessels details'
--SELECT * FROM auth.vessels v;
SELECT v.vessel_id IS NOT NULL AS vessel_id, v.owner_email, v.mmsi, v.name, v.role FROM auth.vessels AS v;
--SELECT * FROM api.metadata m;
\echo 'api.metadata details'
SELECT m.id, m.name, m.mmsi, m.client_id, m.length, m.beam, m.height, m.ship_type, m.plugin_version, m.signalk_version, m.time IS NOT NULL AS time, m.active FROM api.metadata AS m;
SELECT vessel_id IS NOT NULL AS vessel_id_not_null, m.name, m.mmsi, m.length, m.beam, m.height, m.ship_type, m.plugin_version, m.signalk_version, m.time IS NOT NULL AS time, m.active, configuration IS NOT NULL AS configuration_not_null, available_keys FROM api.metadata AS m;
\echo 'api.logs_view'
--SELECT * FROM api.logbook l;

View File

@@ -13,12 +13,12 @@ current_setting |
current_setting |
link vessel and user based on current_setting
-[ RECORD 1 ]----------------------------------------------------------------
-[ RECORD 1 ]----
name | aava
client_id | vessels.urn:mrn:imo:mmsi:787654321
-[ RECORD 2 ]----------------------------------------------------------------
vessel_id | t
-[ RECORD 2 ]----
name | kapla
client_id | vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd
vessel_id | t
auth.accounts details
-[ RECORD 1 ]-----+-----------------------------
@@ -55,32 +55,34 @@ name | aava
role | vessel_role
api.metadata details
-[ RECORD 1 ]---+------------------------------------------------------------------
id | 1
name | kapla
mmsi | 123456789
client_id | vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd
length | 12
beam | 10
height | 24
ship_type | 36
plugin_version | 0.0.1
signalk_version | signalk_version
time | t
active | t
-[ RECORD 2 ]---+------------------------------------------------------------------
id | 2
name | aava
mmsi | 787654321
client_id | vessels.urn:mrn:imo:mmsi:787654321
length | 12
beam | 10
height | 24
ship_type | 37
plugin_version | 1.0.2
signalk_version | 1.20.0
time | t
active | t
-[ RECORD 1 ]----------+----------------
vessel_id_not_null | t
name | kapla
mmsi | 123456789
length | 12
beam | 10
height | 24
ship_type | 36
plugin_version | 0.0.1
signalk_version | signalk_version
time | t
active | t
configuration_not_null | t
available_keys |
-[ RECORD 2 ]----------+----------------
vessel_id_not_null | t
name | aava
mmsi | 787654321
length | 12
beam | 10
height | 24
ship_type | 37
plugin_version | 1.0.2
signalk_version | 1.20.0
time | t
active | t
configuration_not_null | t
available_keys | []
SET
ROLE grafana current_setting
@@ -93,9 +95,9 @@ vessel_id | t
current_user | grafana
current_setting | demo+kapla@openplotter.cloud
-[ RECORD 1 ]--------------------------------------------------------------
-[ RECORD 1 ]--
__text | kapla
__value | vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd
__value | t
auth.vessels details
-[ RECORD 1 ]-----------------------------
@@ -106,19 +108,20 @@ name | kapla
role | vessel_role
api.metadata details
-[ RECORD 1 ]---+------------------------------------------------------------------
id | 1
name | kapla
mmsi | 123456789
client_id | vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd
length | 12
beam | 10
height | 24
ship_type | 36
plugin_version | 0.0.1
signalk_version | signalk_version
time | t
active | t
-[ RECORD 1 ]----------+----------------
vessel_id_not_null | t
name | kapla
mmsi | 123456789
length | 12
beam | 10
height | 24
ship_type | 36
plugin_version | 0.0.1
signalk_version | signalk_version
time | t
active | t
configuration_not_null | t
available_keys |
api.logs_view
-[ RECORD 1 ]----+-----------------------

View File

@@ -22,12 +22,26 @@ SELECT v.vessel_id as "vessel_id" FROM auth.vessels v WHERE v.owner_email = 'dem
--\echo :"vessel_id"
SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
-- Delete logbook for user
-- Count logbook for user
\echo 'logbook'
SELECT count(*) FROM api.logbook WHERE vessel_id = current_setting('vessel.id', false);
\echo 'logbook'
-- track_geom and track_geojson are now dynamic from mobilitydb
SELECT name,_from_time IS NOT NULL AS _from_time, _to_time IS NOT NULL AS _to_time, trajectory(trip) AS track_geom, distance,duration,avg_speed,max_speed,max_wind_speed,notes,extra FROM api.logbook WHERE vessel_id = current_setting('vessel.id', false) ORDER BY id ASC;
SELECT name,_from_time IS NOT NULL AS _from_time_not_null, _to_time IS NOT NULL AS _to_time_not_null, trajectory(trip) AS track_geom, distance,duration,avg_speed,max_speed,max_wind_speed,notes,extra FROM api.logbook WHERE vessel_id = current_setting('vessel.id', false) ORDER BY id ASC;
--
-- user_role
SET ROLE user_role;
\echo 'ROLE user_role current_setting'
SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
-- Count logbook for user
\echo 'logbook'
SELECT count(*) FROM api.logbook;
\echo 'logbook'
-- track_geom and track_geojson are now dynamic from mobilitydb
SELECT name,_from_time IS NOT NULL AS _from_time_not_null, _to_time IS NOT NULL AS _to_time_not_null, trajectory(trip) AS track_geom, distance,duration,avg_speed,max_speed,max_wind_speed,notes,extra FROM api.logbook ORDER BY id ASC;
-- Delete logbook for user
\echo 'Delete logbook for user kapla'

View File

@@ -17,54 +17,113 @@ logbook
count | 4
logbook
-[ RECORD 1 ]--+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | patch log name 3
_from_time | t
_to_time | t
track_geom | 0102000020E61000001A000000B0DEBBE0E68737404DA938FBF0094E4020D26F5F0786374030BB270F0B094E400C6E7ED60F843740AA60545227084E40D60FC48C03823740593CE27D42074E407B39D9F322803740984C158C4A064E4091ED7C3F357E3740898BB63D54054E40A8A1208B477C37404BA3DC9059044E404C5CB4EDA17A3740C4F856115B034E40A9A44E4013793740D8F0F44A59024E40E4839ECDAA773740211FF46C56014E405408D147067637408229F03B73004E40787AA52C43743740F90FE9B7AFFF4D40F8098D4D18723740C217265305FF4D4084E82303537037409A2D464AA0FE4D4022474DCE636F37402912396A72FE4D408351499D806E374088CFB02B40FE4D4076711B0DE06D3740B356C7040FFE4D404EAC66B0BC6E374058A835CD3BFE4D40D7A3703D0A6F3740D3E10EC15EFE4D4087602F277B6E3740A779C7293AFE4D402063EE5A426E3740B5A679C729FE4D40381DEE10EC6D37409ECA7C1A0AFE4D40E2C46A06CB6B37400A43F7BF36FD4D4075931804566E3740320BDAD125FD4D409A2D464AA06E37404A5658830AFD4D40029A081B9E6E37404A5658830AFD4D40
distance | 7.6447
duration | PT27M
avg_speed | 3.6357142857142852
max_speed | 6.1
max_wind_speed | 22.1
notes | new log note 3
extra | {"tags": ["tag_name"], "metrics": {"propulsion.main.runTime": "PT10S"}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": 1}, "avg_wind_speed": 14.549999999999999}
-[ RECORD 2 ]--+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | Norra hamnen to Ekenäs
_from_time | t
_to_time | t
track_geom | 0102000020E610000013000000029A081B9E6E37404A5658830AFD4D404806A6C0EF6C3740DA1B7C6132FD4D40FE65F7E461693740226C787AA5FC4D407DD3E10EC1663740B29DEFA7C6FB4D40898BB63D5465374068479724BCFA4D409A5271F6E1633740B6847CD0B3F94D40431CEBE236623740E9263108ACF84D402C6519E2585F37407E678EBFC7F74D4096218E75715B374027C5B45C23F74D402AA913D044583740968DE1C46AF64D405AF5B9DA8A5537407BEF829B9FF54D407449C2ABD253374086C954C1A8F44D407D1A0AB278543740F2B0506B9AF34D409D11A5BDC15737406688635DDCF24D4061C3D32B655937402CAF6F3ADCF14D408988888888583740B3319C58CDF04D4021FAC8C0145837408C94405DB7EF4D40B8F9593F105B37403DC0804BEDEE4D40DE4C5FE2A25D3740AE47E17A14EE4D40
distance | 8.8968
duration | PT20M
avg_speed | 5.4523809523809526
max_speed | 6.5
max_wind_speed | 37.2
notes |
extra | {"metrics": {"propulsion.main.runTime": "PT11S"}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": -1}, "avg_wind_speed": 10.476190476190478}
-[ RECORD 3 ]--+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | Tropics Zone
_from_time | t
_to_time | t
track_geom | 0102000020E610000002000000A4E85E0D58934FC000DC509B80052C40BC069B43D64553C090510727F3BD2940
distance | 123
duration |
avg_speed |
max_speed |
max_wind_speed |
notes |
extra |
-[ RECORD 4 ]--+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | Alaska Zone
_from_time | t
_to_time | t
track_geom | 0102000020E610000002000000FDB11ED079F261C090C47F1861B84D40D3505124540B63C09C091C1C8D4A4C40
distance | 1234
duration |
avg_speed |
max_speed |
max_wind_speed |
notes |
extra |
-[ RECORD 1 ]-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | patch log name 3
_from_time_not_null | t
_to_time_not_null | t
track_geom | 0102000020E61000001A000000B0DEBBE0E68737404DA938FBF0094E4020D26F5F0786374030BB270F0B094E400C6E7ED60F843740AA60545227084E40D60FC48C03823740593CE27D42074E407B39D9F322803740984C158C4A064E4091ED7C3F357E3740898BB63D54054E40A8A1208B477C37404BA3DC9059044E404C5CB4EDA17A3740C4F856115B034E40A9A44E4013793740D8F0F44A59024E40E4839ECDAA773740211FF46C56014E405408D147067637408229F03B73004E40787AA52C43743740F90FE9B7AFFF4D40F8098D4D18723740C217265305FF4D4084E82303537037409A2D464AA0FE4D4022474DCE636F37402912396A72FE4D408351499D806E374088CFB02B40FE4D4076711B0DE06D3740B356C7040FFE4D404EAC66B0BC6E374058A835CD3BFE4D40D7A3703D0A6F3740D3E10EC15EFE4D4087602F277B6E3740A779C7293AFE4D402063EE5A426E3740B5A679C729FE4D40381DEE10EC6D37409ECA7C1A0AFE4D40E2C46A06CB6B37400A43F7BF36FD4D4075931804566E3740320BDAD125FD4D409A2D464AA06E37404A5658830AFD4D40029A081B9E6E37404A5658830AFD4D40
distance | 7.6447
duration | PT27M
avg_speed | 3.6357142857142852
max_speed | 6.1
max_wind_speed | 22.1
notes | new log note 3
extra | {"tags": ["tag_name"], "metrics": {"propulsion.main.runTime": "PT10S"}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": 1}, "avg_wind_speed": 14.549999999999999}
-[ RECORD 2 ]-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | Norra hamnen to Ekenäs
_from_time_not_null | t
_to_time_not_null | t
track_geom | 0102000020E610000013000000029A081B9E6E37404A5658830AFD4D404806A6C0EF6C3740DA1B7C6132FD4D40FE65F7E461693740226C787AA5FC4D407DD3E10EC1663740B29DEFA7C6FB4D40898BB63D5465374068479724BCFA4D409A5271F6E1633740B6847CD0B3F94D40431CEBE236623740E9263108ACF84D402C6519E2585F37407E678EBFC7F74D4096218E75715B374027C5B45C23F74D402AA913D044583740968DE1C46AF64D405AF5B9DA8A5537407BEF829B9FF54D407449C2ABD253374086C954C1A8F44D407D1A0AB278543740F2B0506B9AF34D409D11A5BDC15737406688635DDCF24D4061C3D32B655937402CAF6F3ADCF14D408988888888583740B3319C58CDF04D4021FAC8C0145837408C94405DB7EF4D40B8F9593F105B37403DC0804BEDEE4D40DE4C5FE2A25D3740AE47E17A14EE4D40
distance | 8.8968
duration | PT20M
avg_speed | 5.4523809523809526
max_speed | 6.5
max_wind_speed | 37.2
notes |
extra | {"metrics": {"propulsion.main.runTime": "PT11S"}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": -1}, "avg_wind_speed": 10.476190476190478}
-[ RECORD 3 ]-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | Tropics Zone
_from_time_not_null | t
_to_time_not_null | t
track_geom | 0102000020E610000002000000A4E85E0D58934FC000DC509B80052C40BC069B43D64553C090510727F3BD2940
distance | 123
duration |
avg_speed |
max_speed |
max_wind_speed |
notes |
extra |
-[ RECORD 4 ]-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | Alaska Zone
_from_time_not_null | t
_to_time_not_null | t
track_geom | 0102000020E610000002000000FDB11ED079F261C090C47F1861B84D40D3505124540B63C09C091C1C8D4A4C40
distance | 1234
duration |
avg_speed |
max_speed |
max_wind_speed |
notes |
extra |
SET
ROLE user_role current_setting
-[ RECORD 1 ]
vessel_id | t
logbook
-[ RECORD 1 ]
count | 4
logbook
-[ RECORD 1 ]-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | patch log name 3
_from_time_not_null | t
_to_time_not_null | t
track_geom | 0102000020E61000001A000000B0DEBBE0E68737404DA938FBF0094E4020D26F5F0786374030BB270F0B094E400C6E7ED60F843740AA60545227084E40D60FC48C03823740593CE27D42074E407B39D9F322803740984C158C4A064E4091ED7C3F357E3740898BB63D54054E40A8A1208B477C37404BA3DC9059044E404C5CB4EDA17A3740C4F856115B034E40A9A44E4013793740D8F0F44A59024E40E4839ECDAA773740211FF46C56014E405408D147067637408229F03B73004E40787AA52C43743740F90FE9B7AFFF4D40F8098D4D18723740C217265305FF4D4084E82303537037409A2D464AA0FE4D4022474DCE636F37402912396A72FE4D408351499D806E374088CFB02B40FE4D4076711B0DE06D3740B356C7040FFE4D404EAC66B0BC6E374058A835CD3BFE4D40D7A3703D0A6F3740D3E10EC15EFE4D4087602F277B6E3740A779C7293AFE4D402063EE5A426E3740B5A679C729FE4D40381DEE10EC6D37409ECA7C1A0AFE4D40E2C46A06CB6B37400A43F7BF36FD4D4075931804566E3740320BDAD125FD4D409A2D464AA06E37404A5658830AFD4D40029A081B9E6E37404A5658830AFD4D40
distance | 7.6447
duration | PT27M
avg_speed | 3.6357142857142852
max_speed | 6.1
max_wind_speed | 22.1
notes | new log note 3
extra | {"tags": ["tag_name"], "metrics": {"propulsion.main.runTime": "PT10S"}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": 1}, "avg_wind_speed": 14.549999999999999}
-[ RECORD 2 ]-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | Norra hamnen to Ekenäs
_from_time_not_null | t
_to_time_not_null | t
track_geom | 0102000020E610000013000000029A081B9E6E37404A5658830AFD4D404806A6C0EF6C3740DA1B7C6132FD4D40FE65F7E461693740226C787AA5FC4D407DD3E10EC1663740B29DEFA7C6FB4D40898BB63D5465374068479724BCFA4D409A5271F6E1633740B6847CD0B3F94D40431CEBE236623740E9263108ACF84D402C6519E2585F37407E678EBFC7F74D4096218E75715B374027C5B45C23F74D402AA913D044583740968DE1C46AF64D405AF5B9DA8A5537407BEF829B9FF54D407449C2ABD253374086C954C1A8F44D407D1A0AB278543740F2B0506B9AF34D409D11A5BDC15737406688635DDCF24D4061C3D32B655937402CAF6F3ADCF14D408988888888583740B3319C58CDF04D4021FAC8C0145837408C94405DB7EF4D40B8F9593F105B37403DC0804BEDEE4D40DE4C5FE2A25D3740AE47E17A14EE4D40
distance | 8.8968
duration | PT20M
avg_speed | 5.4523809523809526
max_speed | 6.5
max_wind_speed | 37.2
notes |
extra | {"metrics": {"propulsion.main.runTime": "PT11S"}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": -1}, "avg_wind_speed": 10.476190476190478}
-[ RECORD 3 ]-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | Tropics Zone
_from_time_not_null | t
_to_time_not_null | t
track_geom | 0102000020E610000002000000A4E85E0D58934FC000DC509B80052C40BC069B43D64553C090510727F3BD2940
distance | 123
duration |
avg_speed |
max_speed |
max_wind_speed |
notes |
extra |
-[ RECORD 4 ]-------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
name | Alaska Zone
_from_time_not_null | t
_to_time_not_null | t
track_geom | 0102000020E610000002000000FDB11ED079F261C090C47F1861B84D40D3505124540B63C09C091C1C8D4A4C40
distance | 1234
duration |
avg_speed |
max_speed |
max_wind_speed |
notes |
extra |
Delete logbook for user kapla
-[ RECORD 1 ]-----+--

84
tests/sql/metadata.sql Normal file
View File

@@ -0,0 +1,84 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
SELECT count(*) as count_eq_2 FROM api.metadata m;
SELECT v.vessel_id as "vessel_id" FROM auth.vessels v WHERE v.owner_email = 'demo+kapla@openplotter.cloud' \gset
--\echo :"vessel_id"
SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
-- user_role
SET ROLE user_role;
\echo 'api.metadata details'
SELECT vessel_id IS NOT NULL AS vessel_id_not_null, m.name, m.mmsi, m.length, m.beam, m.height, m.ship_type, m.plugin_version, m.signalk_version, m.time IS NOT NULL AS time, m.active, configuration, available_keys FROM api.metadata AS m ORDER BY m.name ASC;
\echo 'api.metadata get configuration'
select configuration from api.metadata; --WHERE vessel_id = current_setting('vessel.id', false);
\echo 'api.metadata update configuration'
UPDATE api.metadata SET configuration = '{ "depthKey": "environment.depth.belowTransducer" }'; --WHERE vessel_id = current_setting('vessel.id', false);
\echo 'api.metadata get configuration with new value'
select configuration->'depthKey' AS depthKey, configuration->'update_at' IS NOT NULL AS update_at_not_null from api.metadata; --WHERE vessel_id = current_setting('vessel.id', false);
\echo 'api.metadata get configuration base on update_at value'
select configuration->'depthKey' AS depthKey, configuration->'update_at' IS NOT NULL AS update_at_not_null from api.metadata WHERE vessel_id = current_setting('vessel.id', false) AND configuration->>'update_at' <= to_char(NOW(), 'YYYY-MM-DD"T"HH24:MI:SS"Z"');
-- Upsert make_model on metadata_ext table
\echo 'api.metadata_ext set make_model'
INSERT INTO api.metadata_ext (vessel_id, make_model)
VALUES (current_setting('vessel.id', false), 'my super yacht')
ON CONFLICT (vessel_id) DO UPDATE
SET make_model = EXCLUDED.make_model;
-- Upsert polar on metadata_ext table
\echo 'api.metadata_ext set polar'
INSERT INTO api.metadata_ext (vessel_id, polar)
VALUES (current_setting('vessel.id', false), 'twa/tws;4;6;8;10;12;14;16;20;24\n0;0;0;0;0;0;0;0;0;0')
ON CONFLICT (vessel_id) DO UPDATE
SET polar = EXCLUDED.polar;
-- Upsert image on metadata_ext table
\echo 'api.metadata_ext set image/image_b64'
INSERT INTO api.metadata_ext (vessel_id, image_b64)
VALUES (current_setting('vessel.id', false), 'iVBORw0KGgoAAAANSUhEUgAAAMgAAAAyCAIAAACWMwO2AAABNklEQVR4nO3bwY6CMBiF0XYy7//KzIKk6VBjiMMNk59zVljRIH6WsrBv29bgal93HwA1CYsIYREhLCKERYSwiBAWEcIiQlhECIsIYREhLCKERYSwiBAWEcIiQlhECIsIYREhLCK+7z6A/6j33lq75G8m')
ON CONFLICT (vessel_id) DO UPDATE
SET image_b64 = EXCLUDED.image_b64;
-- Ensure make_model on metadata_ext table is updated
\echo 'api.metadata_ext get make_model'
SELECT make_model FROM api.metadata_ext; --WHERE vessel_id = current_setting('vessel.id', false);
-- Ensure polar_updated_at on metadata_ext table is updated by trigger
\echo 'api.metadata_ext get polar_updated_at'
SELECT polar,polar_updated_at IS NOT NULL AS polar_updated_at_not_null FROM api.metadata_ext; --WHERE vessel_id = current_setting('vessel.id', false);
-- Ensure image_updated_at on metadata_ext table is updated by trigger
\echo 'api.metadata_ext get image_updated_at'
SELECT image_b64 IS NULL AS image_b64_is_null,image IS NOT NULL AS image_not_null,image_updated_at IS NOT NULL AS image_updated_at_not_null FROM api.metadata_ext; --WHERE vessel_id = current_setting('vessel.id', false);
-- vessel_role
SET ROLE vessel_role;
\echo 'api.metadata get configuration with new value as vessel'
select configuration->'depthKey' AS depthKey, configuration->'update_at' IS NOT NULL AS update_at_not_null from api.metadata; -- WHERE vessel_id = current_setting('vessel.id', false);
\echo 'api.metadata get configuration base on update_at value as vessel'
select configuration->'depthKey' AS depthKey, configuration->'update_at' IS NOT NULL AS update_at_not_null from api.metadata WHERE vessel_id = current_setting('vessel.id', false) AND configuration->>'update_at' <= to_char(NOW(), 'YYYY-MM-DD"T"HH24:MI:SS"Z"');
-- api_anonymous
SET ROLE api_anonymous;
\echo 'api_anonymous get vessel image'
SELECT api.vessel_image(current_setting('vessel.id', false)) IS NOT NULL AS vessel_image_not_null;

View File

@@ -0,0 +1,83 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
-[ RECORD 1 ]-
count_eq_2 | 2
-[ RECORD 1 ]
vessel_id | t
SET
api.metadata details
-[ RECORD 1 ]------+----------------
vessel_id_not_null | t
name | kapla
mmsi | 123456789
length | 12
beam | 10
height | 24
ship_type | 36
plugin_version | 0.0.1
signalk_version | signalk_version
time | t
active | t
configuration |
available_keys |
api.metadata get configuration
-[ RECORD 1 ]-+-
configuration |
api.metadata update configuration
UPDATE 1
api.metadata get configuration with new value
-[ RECORD 1 ]------+------------------------------------
depthkey | "environment.depth.belowTransducer"
update_at_not_null | t
api.metadata get configuration base on update_at value
-[ RECORD 1 ]------+------------------------------------
depthkey | "environment.depth.belowTransducer"
update_at_not_null | t
api.metadata_ext set make_model
INSERT 0 1
api.metadata_ext set polar
INSERT 0 1
api.metadata_ext set image/image_b64
INSERT 0 1
api.metadata_ext get make_model
-[ RECORD 1 ]--------------
make_model | my super yacht
api.metadata_ext get polar_updated_at
-[ RECORD 1 ]-------------+-----------------------------------------------------
polar | twa/tws;4;6;8;10;12;14;16;20;24\n0;0;0;0;0;0;0;0;0;0
polar_updated_at_not_null | t
api.metadata_ext get image_updated_at
-[ RECORD 1 ]-------------+--
image_b64_is_null | f
image_not_null | t
image_updated_at_not_null | t
SET
api.metadata get configuration with new value as vessel
-[ RECORD 1 ]------+------------------------------------
depthkey | "environment.depth.belowTransducer"
update_at_not_null | t
api.metadata get configuration base on update_at value as vessel
-[ RECORD 1 ]------+------------------------------------
depthkey | "environment.depth.belowTransducer"
update_at_not_null | t
SET
api_anonymous get vessel image
-[ RECORD 1 ]---------+--
vessel_image_not_null | t

View File

@@ -67,7 +67,21 @@ SELECT set_config('vessel.id', :'vessel_id_kapla', false) IS NOT NULL as vessel_
-- Export timelapse as Geometry LineString from a trip
\echo 'Export timelapse as Geometry LineString from a trip'
SELECT api.export_logbooks_geojson_linestring_trips_fn(1,2) FROM api.logbook WHERE vessel_id = current_setting('vessel.id', false);
--SELECT api.export_logbooks_geojson_linestring_trips_fn(1,2) FROM api.logbook WHERE vessel_id = current_setting('vessel.id', false);
-- Test geometry_type and num_properties
-- propoerties include endtimestamp and starttimestamp
WITH geojson_output AS (
SELECT api.export_logbooks_geojson_linestring_trips_fn(1, 2) AS geojson
FROM api.logbook
WHERE vessel_id = current_setting('vessel.id', false)
)
SELECT
--geojson
geojson->'features'->0->'geometry'->>'type' AS geometry_type,
--jsonb_array_length(jsonb_object_keys(geojson->'features'->0->'properties')::JSONB),
--jsonb_array_length(jsonb_object_keys(geojson->'features')) AS num_geometry,
(SELECT COUNT(*) FROM jsonb_object_keys(geojson->'features'->0->'properties')) AS num_properties
FROM geojson_output;
-- Export timelapse as Geometry Point from a trip
\echo 'Export timelapse as Geometry Point from a trip'

View File

@@ -52,8 +52,9 @@ Export KML from a trip
vessel_id | t
Export timelapse as Geometry LineString from a trip
-[ RECORD 1 ]-------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
export_logbooks_geojson_linestring_trips_fn | {"type": "FeatureCollection", "features": [{"type": "Feature", "geometry": {"type": "LineString", "coordinates": [[23.530866667, 60.077666667], [23.52355, 60.07065], [23.515866667, 60.0637], [23.507866667, 60.056716667], [23.500533333, 60.04915], [23.493, 60.041633333], [23.485466667, 60.033983333], [23.479033333, 60.026216667], [23.47295, 60.01835], [23.461033333, 60.003516667], [23.45415, 59.99755], [23.445683333, 59.99235], [23.438766667, 59.989266667], [23.435116667, 59.987866667], [23.43165, 59.986333333], [23.4292, 59.984833333], [23.432566667, 59.9862], [23.43375, 59.987266667], [23.431566667, 59.98615], [23.4307, 59.98565], [23.429383333, 59.984683333], [23.421066667, 59.978233333], [23.431, 59.977716667], [23.432133333, 59.976883333], [23.4321, 59.976883333], [23.425533333, 59.9781], [23.41165, 59.9738], [23.401383333, 59.967], [23.395816667, 59.958866667], [23.390166667, 59.9508], [23.38365, 59.94275], [23.37245, 59.935783333], [23.3572, 59.930766667], [23.33415, 59.918933333], [23.327433333, 59.9114], [23.329966667, 59.90315], [23.3428, 59.89735], [23.3492, 59.889533333], [23.345833333, 59.881266667], [23.344066667, 59.872783333], [23.355716667, 59.866616667], [23.365766667, 59.86]]}, "properties": {}}]}
-[ RECORD 1 ]--+-----------
geometry_type | LineString
num_properties | 35
Export timelapse as Geometry Point from a trip
-[ RECORD 1 ]

50
tests/sql/stats.sql Normal file
View File

@@ -0,0 +1,50 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
\echo 'Validate Stats operation'
-- Assign vessel_id var
SELECT v.vessel_id as "vessel_id_kapla" FROM auth.vessels v WHERE v.owner_email = 'demo+kapla@openplotter.cloud' \gset
SELECT v.vessel_id as "vessel_id_aava" FROM auth.vessels v WHERE v.owner_email = 'demo+aava@openplotter.cloud' \gset
-- user_role
SET ROLE user_role;
\echo 'ROLE user_role current_setting'
SELECT set_config('vessel.id', :'vessel_id_kapla', false) IS NOT NULL as vessel_id;
-- Stats logbook and moorages for user
\echo 'Stats logbook and moorages for user kapla'
--SELECT api.stats_fn();
WITH tbl as (SELECT api.stats_fn() as stats)
SELECT tbl.stats->'stats_logs'->>'name' = 'kapla' AS boat_name,
(tbl.stats->'stats_logs'->>'count')::int = 1 AS logs_count,
(tbl.stats->'stats_logs'->>'max_speed')::numeric = 6.5 AS max_speed,
(tbl.stats->'stats_moorages'->>'home_ports')::int = 1 AS home_ports,
(tbl.stats->'stats_moorages'->>'unique_moorages')::numeric = 5 AS unique_moorages,
(tbl.stats->'moorages_top_countries') = '["fi"]' AS moorages_top_countries
FROM tbl;
SELECT set_config('vessel.id', :'vessel_id_aava', false) IS NOT NULL as vessel_id;
-- Stats logbook and moorages for user
\echo 'Stats logbook and moorages for user aava'
--SELECT api.stats_fn();
WITH tbl as (SELECT api.stats_fn() as stats)
SELECT tbl.stats->'stats_logs'->>'name' = 'aava' AS boat_name,
(tbl.stats->'stats_logs'->>'count')::int = 2 AS logs_count,
(tbl.stats->'stats_logs'->>'max_speed')::numeric = 9.5 AS max_speed,
(tbl.stats->'stats_moorages'->>'home_ports')::int = 1 AS home_ports,
(tbl.stats->'stats_moorages'->>'unique_moorages')::numeric = 4 AS unique_moorages,
(tbl.stats->'moorages_top_countries') = '["ee"]' AS moorages_top_countries
FROM tbl;

View File

@@ -0,0 +1,34 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
Validate Stats operation
SET
ROLE user_role current_setting
-[ RECORD 1 ]
vessel_id | t
Stats logbook and moorages for user kapla
-[ RECORD 1 ]----------+--
boat_name | t
logs_count | t
max_speed | t
home_ports | t
unique_moorages | t
moorages_top_countries | t
-[ RECORD 1 ]
vessel_id | t
Stats logbook and moorages for user aava
-[ RECORD 1 ]----------+--
boat_name | t
logs_count | t
max_speed | t
home_ports | t
unique_moorages | t
moorages_top_countries | t

47
tests/sql/stays_ext.sql Normal file
View File

@@ -0,0 +1,47 @@
---------------------------------------------------------------------------
-- Listing
--
-- List current database
select current_database();
-- connect to the DB
\c signalk
-- output display format
\x on
SELECT count(*) as count_eq_0 FROM api.stays_ext m;
SELECT v.vessel_id as "vessel_id" FROM auth.vessels v WHERE v.owner_email = 'demo+kapla@openplotter.cloud' \gset
--\echo :"vessel_id"
SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
-- user_role
SET ROLE user_role;
\echo 'api.stays details'
SELECT vessel_id IS NOT NULL AS vessel_id_not_null, m.name IS NOT NULL AS name_not_null FROM api.stays AS m WHERE active IS False ORDER BY m.name ASC;
-- Upsert image on stays_ext table
\echo 'api.stays_ext set image/image_b64'
INSERT INTO api.stays_ext (vessel_id, stay_id, image_b64)
VALUES (current_setting('vessel.id', false), 1, 'iVBORw0KGgoAAAANSUhEUgAAAMgAAAAyCAIAAACWMwO2AAABNklEQVR4nO3bwY6CMBiF0XYy7//KzIKk6VBjiMMNk59zVljRIH6WsrBv29bgal93HwA1CYsIYREhLCKERYSwiBAWEcIiQlhECIsIYREhLCKERYSwiBAWEcIiQlhECIsIYREhLCK+7z6A/6j33lq75G8m')
ON CONFLICT (stay_id) DO UPDATE
SET image_b64 = EXCLUDED.image_b64;
-- Ensure image_updated_at on metadata_ext table is updated by trigger
\echo 'api.stays_ext get image_updated_at'
SELECT image_b64 IS NULL AS image_b64_is_null,image IS NOT NULL AS image_not_null,image_updated_at IS NOT NULL AS image_updated_at_not_null FROM api.metadata_ext; --WHERE vessel_id = current_setting('vessel.id', false);
-- vessel_role
SET ROLE vessel_role;
\echo 'api.stays_ext'
SELECT vessel_id IS NOT NULL AS vessel_id_not_null, stay_id FROM api.stays_ext;
-- api_anonymous
SET ROLE api_anonymous;
\echo 'api_anonymous get stays image'
SELECT api.stays_image(current_setting('vessel.id', false), 1) IS NOT NULL AS stays_image_not_null;

View File

@@ -0,0 +1,37 @@
current_database
------------------
signalk
(1 row)
You are now connected to database "signalk" as user "username".
Expanded display is on.
-[ RECORD 1 ]-
count_eq_0 | 0
-[ RECORD 1 ]
vessel_id | t
SET
api.stays details
-[ RECORD 1 ]------+--
vessel_id_not_null | t
name_not_null | t
-[ RECORD 2 ]------+--
vessel_id_not_null | t
name_not_null | t
api.stays_ext set image/image_b64
INSERT 0 1
api.stays_ext get image_updated_at
-[ RECORD 1 ]-------------+--
image_b64_is_null | f
image_not_null | t
image_updated_at_not_null | t
SET
api.stays_ext
SET
api_anonymous get stays image
-[ RECORD 1 ]--------+--
stays_image_not_null | t

View File

@@ -5,11 +5,11 @@
You are now connected to database "signalk" as user "username".
Expanded display is on.
-[ RECORD 1 ]--+-------------------------------
server_version | 16.6 (Debian 16.6-1.pgdg120+1)
-[ RECORD 1 ]--+--------------------------------
server_version | 16.10 (Debian 16.10-1.pgdg12+1)
-[ RECORD 1 ]--------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
postgis_full_version | POSTGIS="3.5.1 48ab069" [EXTENSION] PGSQL="160" GEOS="3.11.1-CAPI-1.17.1" PROJ="9.1.1 NETWORK_ENABLED=OFF URL_ENDPOINT=https://cdn.proj.org USER_WRITABLE_DIRECTORY=/var/lib/postgresql/.local/share/proj DATABASE_PATH=/usr/share/proj/proj.db" (compiled against PROJ 9.1.1) LIBXML="2.9.14" LIBJSON="0.16" LIBPROTOBUF="1.4.1" WAGYU="0.5.0 (Internal)"
postgis_full_version | POSTGIS="3.5.3 aab5f55" [EXTENSION] PGSQL="160" GEOS="3.11.1-CAPI-1.17.1" PROJ="9.1.1 NETWORK_ENABLED=OFF URL_ENDPOINT=https://cdn.proj.org USER_WRITABLE_DIRECTORY=/var/lib/postgresql/.local/share/proj DATABASE_PATH=/usr/share/proj/proj.db" (compiled against PROJ 9.1.1) LIBXML="2.9.14" LIBJSON="0.16" LIBPROTOBUF="1.4.1" WAGYU="0.5.0 (Internal)"
-[ RECORD 1 ]--------------------------------------------------------------------------------------
Name | citext
@@ -53,15 +53,20 @@ Schema | pg_catalog
Description | PL/Python3U untrusted procedural language
-[ RECORD 9 ]--------------------------------------------------------------------------------------
Name | postgis
Version | 3.5.1
Version | 3.5.3
Schema | public
Description | PostGIS geometry and geography spatial types and functions
-[ RECORD 10 ]-------------------------------------------------------------------------------------
Name | timescaledb
Version | 2.17.2
Version | 2.21.3
Schema | public
Description | Enables scalable inserts and complex queries for time-series data (Community Edition)
-[ RECORD 11 ]-------------------------------------------------------------------------------------
Name | timescaledb_toolkit
Version | 1.21.0
Schema | public
Description | Library of analytical hyperfunctions, time-series pipelining, and other SQL utilities
-[ RECORD 12 ]-------------------------------------------------------------------------------------
Name | uuid-ossp
Version | 1.1
Schema | public
@@ -111,14 +116,14 @@ laninline | 13566
lanvalidator | 13567
lanacl |
-[ RECORD 5 ]-+-----------
oid | 18190
oid | 18251
lanname | plpython3u
lanowner | 10
lanispl | t
lanpltrusted | t
lanplcallfoid | 18187
laninline | 18188
lanvalidator | 18189
lanplcallfoid | 18248
laninline | 18249
lanvalidator | 18250
lanacl |
-[ RECORD 1 ]+-----------
@@ -214,18 +219,22 @@ Name | spatial_ref_sys
Type | table
Owner | username
-[ RECORD 1 ]--------
-[ RECORD 1 ]------------
schema_api | logbook
-[ RECORD 2 ]--------
-[ RECORD 2 ]------------
schema_api | metadata
-[ RECORD 3 ]--------
-[ RECORD 3 ]------------
schema_api | metadata_ext
-[ RECORD 4 ]------------
schema_api | metrics
-[ RECORD 4 ]--------
-[ RECORD 5 ]------------
schema_api | moorages
-[ RECORD 5 ]--------
-[ RECORD 6 ]------------
schema_api | stays
-[ RECORD 6 ]--------
-[ RECORD 7 ]------------
schema_api | stays_at
-[ RECORD 8 ]------------
schema_api | stays_ext
-[ RECORD 1 ]-+------------------------------
schema_public | aistypes
@@ -274,31 +283,13 @@ with_check | true
-[ RECORD 2 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | true
-[ RECORD 3 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, true))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 4 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | api_scheduler_role
permissive | PERMISSIVE
roles | {scheduler}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 5 ]------------------------------------------------------------------------------------------------------------------------------
-[ RECORD 3 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | grafana_role
@@ -307,7 +298,7 @@ roles | {grafana}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 6 ]------------------------------------------------------------------------------------------------------------------------------
-[ RECORD 4 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | grafana_proxy_role
@@ -316,52 +307,34 @@ roles | {grafana_auth}
cmd | ALL
qual | true
with_check | false
-[ RECORD 5 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 6 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | vessels
policyname | grafana_proxy_role
permissive | PERMISSIVE
roles | {grafana_auth}
cmd | ALL
qual | true
with_check | false
-[ RECORD 7 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 8 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | true
-[ RECORD 9 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | vessels
policyname | grafana_proxy_role
permissive | PERMISSIVE
roles | {grafana_auth}
cmd | ALL
qual | true
with_check | false
-[ RECORD 10 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, true))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 11 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | api_scheduler_role
permissive | PERMISSIVE
roles | {scheduler}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 12 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 8 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | grafana_role
@@ -370,7 +343,7 @@ roles | {grafana}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 13 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 9 ]------------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | api_anonymous_role
@@ -379,7 +352,7 @@ roles | {api_anonymous}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 14 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 10 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | admin_all
@@ -388,7 +361,7 @@ roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 15 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 11 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | api_vessel_role
@@ -397,7 +370,7 @@ roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | true
-[ RECORD 16 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 12 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | accounts
policyname | admin_all
@@ -406,7 +379,7 @@ roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 17 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 13 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | api_user_role
@@ -415,7 +388,7 @@ roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, true))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 18 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 14 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | api_scheduler_role
@@ -424,7 +397,7 @@ roles | {scheduler}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 19 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 15 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | grafana_role
@@ -433,7 +406,7 @@ roles | {grafana}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 20 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 16 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | api_anonymous_role
@@ -442,7 +415,7 @@ roles | {api_anonymous}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 21 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 17 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | admin_all
@@ -451,7 +424,7 @@ roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 22 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 18 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | api_vessel_role
@@ -460,7 +433,7 @@ roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | true
-[ RECORD 23 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 19 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | logbook
policyname | logbook_qgis_role
@@ -469,7 +442,7 @@ roles | {qgis_role}
cmd | ALL
qual | true
with_check | false
-[ RECORD 24 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 20 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | public
tablename | process_queue
policyname | public_maplapse_role
@@ -478,7 +451,7 @@ roles | {maplapse_role}
cmd | ALL
qual | true
with_check | true
-[ RECORD 25 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 21 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | api_user_role
@@ -487,7 +460,7 @@ roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, true))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 26 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 22 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | api_scheduler_role
@@ -496,7 +469,7 @@ roles | {scheduler}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 27 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 23 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | grafana_role
@@ -505,7 +478,7 @@ roles | {grafana}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 28 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 24 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays
policyname | api_anonymous_role
@@ -514,7 +487,7 @@ roles | {api_anonymous}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 29 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 25 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | admin_all
@@ -523,7 +496,7 @@ roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 30 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 26 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | api_vessel_role
@@ -532,7 +505,16 @@ roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | true
-[ RECORD 31 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 27 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays_ext
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 28 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | api_user_role
@@ -541,7 +523,7 @@ roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, true))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 32 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 29 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | api_scheduler_role
@@ -550,7 +532,7 @@ roles | {scheduler}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 33 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 30 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | grafana_role
@@ -559,7 +541,7 @@ roles | {grafana}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 34 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 31 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | moorages
policyname | api_anonymous_role
@@ -568,7 +550,7 @@ roles | {api_anonymous}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | false
-[ RECORD 35 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 32 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | vessels
policyname | admin_all
@@ -577,7 +559,7 @@ roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 36 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 33 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | vessels
policyname | api_user_role
@@ -586,7 +568,7 @@ roles | {user_role}
cmd | ALL
qual | ((vessel_id = current_setting('vessel.id'::text, true)) AND ((owner_email)::text = current_setting('user.email'::text, true)))
with_check | ((vessel_id = current_setting('vessel.id'::text, true)) AND ((owner_email)::text = current_setting('user.email'::text, true)))
-[ RECORD 37 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 34 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | vessels
policyname | grafana_role
@@ -595,7 +577,7 @@ roles | {grafana}
cmd | ALL
qual | ((owner_email)::text = current_setting('user.email'::text, true))
with_check | false
-[ RECORD 38 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 35 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | accounts
policyname | api_user_role
@@ -604,7 +586,7 @@ roles | {user_role}
cmd | ALL
qual | ((email)::text = current_setting('user.email'::text, true))
with_check | ((email)::text = current_setting('user.email'::text, true))
-[ RECORD 39 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 36 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | accounts
policyname | api_scheduler_role
@@ -613,7 +595,7 @@ roles | {scheduler}
cmd | ALL
qual | ((email)::text = current_setting('user.email'::text, true))
with_check | ((email)::text = current_setting('user.email'::text, true))
-[ RECORD 40 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 37 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | auth
tablename | accounts
policyname | grafana_proxy_role
@@ -622,7 +604,7 @@ roles | {grafana_auth}
cmd | ALL
qual | true
with_check | false
-[ RECORD 41 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 38 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | public
tablename | process_queue
policyname | admin_all
@@ -631,7 +613,7 @@ roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 42 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 39 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | public
tablename | process_queue
policyname | api_vessel_role
@@ -640,7 +622,7 @@ roles | {vessel_role}
cmd | ALL
qual | ((ref_id = current_setting('user.id'::text, true)) OR (ref_id = current_setting('vessel.id'::text, true)))
with_check | true
-[ RECORD 43 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 40 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | public
tablename | process_queue
policyname | api_user_role
@@ -649,7 +631,7 @@ roles | {user_role}
cmd | ALL
qual | ((ref_id = current_setting('user.id'::text, true)) OR (ref_id = current_setting('vessel.id'::text, true)))
with_check | ((ref_id = current_setting('user.id'::text, true)) OR (ref_id = current_setting('vessel.id'::text, true)))
-[ RECORD 44 ]-----------------------------------------------------------------------------------------------------------------------------
-[ RECORD 41 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | public
tablename | process_queue
policyname | api_scheduler_role
@@ -658,6 +640,105 @@ roles | {scheduler}
cmd | ALL
qual | true
with_check | false
-[ RECORD 42 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays_ext
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 43 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays_ext
policyname | api_anonymous_role
permissive | PERMISSIVE
roles | {api_anonymous}
cmd | ALL
qual | true
with_check | false
-[ RECORD 44 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | stays_ext
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | false
with_check | false
-[ RECORD 45 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata_ext
policyname | admin_all
permissive | PERMISSIVE
roles | {username}
cmd | ALL
qual | true
with_check | true
-[ RECORD 46 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata_ext
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 47 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata_ext
policyname | api_anonymous_role
permissive | PERMISSIVE
roles | {api_anonymous}
cmd | ALL
qual | true
with_check | false
-[ RECORD 48 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata_ext
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | false
with_check | false
-[ RECORD 49 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 50 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | api_vessel_role
permissive | PERMISSIVE
roles | {vessel_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 51 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metadata
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
-[ RECORD 52 ]-----------------------------------------------------------------------------------------------------------------------------
schemaname | api
tablename | metrics
policyname | api_user_role
permissive | PERMISSIVE
roles | {user_role}
cmd | ALL
qual | (vessel_id = current_setting('vessel.id'::text, false))
with_check | (vessel_id = current_setting('vessel.id'::text, false))
Test nominatim reverse_geocode_py_fn
-[ RECORD 1 ]---------+----------------------------------------
@@ -671,16 +752,16 @@ overpass_py_fn | {"fee": "yes", "vhf": "09", "name": "Port Olímpic", "phone": "
-[ RECORD 1 ]--+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
overpass_py_fn | {"name": "Port de la Ginesta", "type": "multipolygon", "leisure": "marina", "name:ca": "Port de la Ginesta", "wikidata": "Q16621038", "wikipedia": "ca:Port Ginesta", "check_date": "2024-08-23"}
-[ RECORD 1 ]--+----------------------------------------------
overpass_py_fn | {"name": "Norra hamnen", "leisure": "marina"}
-[ RECORD 1 ]--+---------------------------------------------------------------------------------------------------------------
overpass_py_fn | {"name": "Norra hamnen", "leisure": "marina", "seamark:type": "harbour", "seamark:harbour:category": "marina"}
-[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------------------------------------------------------------
versions_fn | {"api_version" : "0.8.1", "sys_version" : "PostgreSQL 16.6", "mobilitydb" : "1.2.0", "timescaledb" : "2.17.2", "postgis" : "3.5.1", "postgrest" : "PostgREST 12.2.3"}
-[ RECORD 1 ]-----------------------------------------------------------------------------------------------------------------------------------------------------------------------
versions_fn | {"api_version" : "0.9.4", "sys_version" : "PostgreSQL 16.10", "mobilitydb" : "1.2.0", "timescaledb" : "2.21.3", "postgis" : "3.5.3", "postgrest" : "PostgREST 13.0.6"}
-[ RECORD 1 ]-----------------
api_version | 0.8.1
sys_version | PostgreSQL 16.6
timescaledb | 2.17.2
postgis | 3.5.1
postgrest | PostgREST 12.2.3
api_version | 0.9.4
sys_version | PostgreSQL 16.10
timescaledb | 2.21.3
postgis | 3.5.3
postgrest | PostgREST 13.0.6

View File

@@ -14,15 +14,6 @@ if [[ ! -x "/usr/bin/psql" ]]; then
apt update && apt -y install postgresql-client
fi
# go install
if [[ ! -x "/usr/bin/go" || ! -x "/root/go/bin/mermerd" ]]; then
#wget -q https://go.dev/dl/go1.21.4.linux-arm64.tar.gz && \
#rm -rf /usr/local/go && tar -C /usr/local -xzf go1.21.4.linux-arm64.tar.gz && \
apt update && apt -y install golang-go && \
#go install github.com/KarnerTh/mermerd@latest require latest go version
go install github.com/KarnerTh/mermerd@v0.11.0
fi
# pnpm install
if [[ ! -x "/usr/local/bin/pnpm" ]]; then
npm install -g pnpm
@@ -49,6 +40,19 @@ else
exit 1
fi
# metadata and vessel configuration unit tests
psql ${PGSAIL_DB_URI} < sql/metadata.sql > output/metadata.sql.output
diff sql/metadata.sql.output output/metadata.sql.output > /dev/null
#diff -u sql/metadata.sql.output output/metadata.sql.output | wc -l
#echo 0
if [ $? -eq 0 ]; then
echo OK
else
echo SQL metadata.sql FAILED
diff -u sql/metadata.sql.output output/metadata.sql.output
exit 1
fi
# https://www.postgresql.org/docs/current/app-psql.html
# run cron jobs
#psql -U ${POSTGRES_USER} -h 172.30.0.1 signalk < sql/cron_run_jobs.sql > output/cron_run_jobs.sql.output
@@ -126,6 +130,19 @@ else
exit
fi
# Stays extended unit tests
psql ${PGSAIL_DB_URI} < sql/stays_ext.sql > output/stays_ext.sql.output
diff sql/stays_ext.sql.output output/stays_ext.sql.output > /dev/null
#diff -u sql/stays_ext.sql.output output/stays_ext.sql.output | wc -l
#echo 0
if [ $? -eq 0 ]; then
echo OK
else
echo SQL stays_ext.sql FAILED
diff -u sql/stays_ext.sql.output output/stays_ext.sql.output
exit 1
fi
# Summary unit tests
psql ${PGSAIL_DB_URI} < sql/summary.sql > output/summary.sql.output
diff sql/summary.sql.output output/summary.sql.output > /dev/null
@@ -205,17 +222,17 @@ else
fi
# Stats SQL unit tests
#psql ${PGSAIL_DB_URI} < sql/stats.sql > output/stats.sql.output
#diff sql/stats.sql.output output/stats.sql.output > /dev/null
psql ${PGSAIL_DB_URI} < sql/stats.sql > output/stats.sql.output
diff sql/stats.sql.output output/stats.sql.output > /dev/null
#diff -u sql/stats.sql.output output/stats.sql.output | wc -l
#echo 0
#if [ $? -eq 0 ]; then
# echo SQL stats.sql OK
#else
# echo SQL stats.sql FAILED
# diff -u sql/stats.sql.output output/stats.sql.output
# exit 1
#fi
if [ $? -eq 0 ]; then
echo SQL stats.sql OK
else
echo SQL stats.sql FAILED
diff -u sql/stats.sql.output output/stats.sql.output
exit 1
fi
# MobilityDB SQL unit tests
psql ${PGSAIL_DB_URI} < sql/mobilitydb.sql > output/mobilitydb.sql.output
@@ -266,17 +283,3 @@ else
echo openapi.json FAILED
exit 1
fi
# Generate and update mermaid schema documentation
/root/go/bin/mermerd --runConfig ../docs/ERD/mermerdConfig.yaml
#echo $?
echo 0 # not working in github-actions
if [ $? -eq 0 ]; then
cp postgsail.md ../docs/ERD/postgsail.md
echo postgsail.md OK
else
echo postgsail.md FAILED
exit 1
fi
#npm i -D schemalint && npx schemalint