Compare commits
224 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
22466430ac | ||
![]() |
643c16ad3f | ||
![]() |
59a3c41b4a | ||
![]() |
371eb6c720 | ||
![]() |
c2ffe9777c | ||
![]() |
c0261791f5 | ||
![]() |
e727954f83 | ||
![]() |
22b04334f8 | ||
![]() |
a5ec4c0039 | ||
![]() |
6a63f7d02f | ||
![]() |
2fec2e650c | ||
![]() |
ed8514bfb1 | ||
![]() |
f25e735674 | ||
![]() |
c1b71cabd8 | ||
![]() |
495e25b838 | ||
![]() |
a547271496 | ||
![]() |
3a1d0baef8 | ||
![]() |
628de57b5f | ||
![]() |
9a5f27d21e | ||
![]() |
7892b615e0 | ||
![]() |
2c2f5d8605 | ||
![]() |
47eda3dcaf | ||
![]() |
0c0279767f | ||
![]() |
98e28aacea | ||
![]() |
b04d336c0d | ||
![]() |
288c458c5a | ||
![]() |
d3dd46c834 | ||
![]() |
d0bc468ce7 | ||
![]() |
3f51e89303 | ||
![]() |
000c5651e2 | ||
![]() |
4bec738826 | ||
![]() |
012812c898 | ||
![]() |
1c04822cf8 | ||
![]() |
50f018100b | ||
![]() |
13c461a038 | ||
![]() |
8763448523 | ||
![]() |
e02aaf3676 | ||
![]() |
ae61072ba4 | ||
![]() |
242a5554ea | ||
![]() |
39888e1957 | ||
![]() |
666f69c42a | ||
![]() |
77f41251c5 | ||
![]() |
40a1e0fa39 | ||
![]() |
5cf2d10757 | ||
![]() |
0dd6410589 | ||
![]() |
682c68a108 | ||
![]() |
5d1db984b8 | ||
![]() |
0a09d7bbfc | ||
![]() |
14cc4f5ed2 | ||
![]() |
ff23f5c2ad | ||
![]() |
489fb9562b | ||
![]() |
8c32345342 | ||
![]() |
480417917d | ||
![]() |
e557ed49a5 | ||
![]() |
a3475dfe99 | ||
![]() |
e670e11cd5 | ||
![]() |
88de5003c2 | ||
![]() |
c46a428fc2 | ||
![]() |
db3bd6b06f | ||
![]() |
4ea7a1b019 | ||
![]() |
ce106074dc | ||
![]() |
e7d8229e83 | ||
![]() |
f14342bb07 | ||
![]() |
c4fbf7682d | ||
![]() |
f8c1f43f48 | ||
![]() |
0d5089af2d | ||
![]() |
da1952ed31 | ||
![]() |
a5d5585366 | ||
![]() |
5f9a889a44 | ||
![]() |
f9719bd174 | ||
![]() |
8d1b8cb389 | ||
![]() |
acfd058d3b | ||
![]() |
eeae7c40c6 | ||
![]() |
2bbf27f3ad | ||
![]() |
2ba81a935f | ||
![]() |
0fbac67895 | ||
![]() |
228b234582 | ||
![]() |
75c8a9506a | ||
![]() |
2b48a66cd2 | ||
![]() |
e642049e93 | ||
![]() |
94e123c95e | ||
![]() |
9787328990 | ||
![]() |
de62d936d5 | ||
![]() |
293a33da08 | ||
![]() |
2b105db5c7 | ||
![]() |
af003d5a62 | ||
![]() |
ecc9fd6d9f | ||
![]() |
df5f667b41 | ||
![]() |
1bfa04a057 | ||
![]() |
a1e8827479 | ||
![]() |
d837dc57fb | ||
![]() |
90e8b24321 | ||
![]() |
a1ccfd5f7c | ||
![]() |
6ecb345758 | ||
![]() |
0709bc83c4 | ||
![]() |
8c777cd028 | ||
![]() |
cfe3105f87 | ||
![]() |
5af24a1878 | ||
![]() |
0aae8d002b | ||
![]() |
22c69a2fd9 | ||
![]() |
c5f1b85a16 | ||
![]() |
0157fe12e5 | ||
![]() |
ead2b99e7f | ||
![]() |
711d5a0d40 | ||
![]() |
7e52065ef8 | ||
![]() |
f65873db81 | ||
![]() |
347299d76e | ||
![]() |
effeb29915 | ||
![]() |
9329a6d04b | ||
![]() |
70be4fb295 | ||
![]() |
5960447297 | ||
![]() |
f240222b98 | ||
![]() |
0218f2fa73 | ||
![]() |
79a96c7556 | ||
![]() |
21f96483f5 | ||
![]() |
4c6d6290f0 | ||
![]() |
dc02dc886d | ||
![]() |
6355f98792 | ||
![]() |
7543c93dcf | ||
![]() |
d76964f3db | ||
![]() |
de651ea7ab | ||
![]() |
fddd3df05e | ||
![]() |
472131efbd | ||
![]() |
1f7bb433e2 | ||
![]() |
c4fa9f5512 | ||
![]() |
b005f592e9 | ||
![]() |
6cc13313f1 | ||
![]() |
fdb466abde | ||
![]() |
5f0adb67c8 | ||
![]() |
2e170c5480 | ||
![]() |
5dd2875b91 | ||
![]() |
ee131e0e70 | ||
![]() |
6426e14d54 | ||
![]() |
8fff17dee3 | ||
![]() |
eb8ba54230 | ||
![]() |
f9ed13761c | ||
![]() |
e1e7da779e | ||
![]() |
c879c4bdab | ||
![]() |
e5f2469358 | ||
![]() |
480409de12 | ||
![]() |
9d8a7294e0 | ||
![]() |
e3ae6b4243 | ||
![]() |
268ce5b908 | ||
![]() |
ce55a58c87 | ||
![]() |
14e2103e0f | ||
![]() |
8025fc4d52 | ||
![]() |
117bdd2e3f | ||
![]() |
b37c33bccb | ||
![]() |
2507545d3f | ||
![]() |
b6ef06d382 | ||
![]() |
30de9b76af | ||
![]() |
8f1558f436 | ||
![]() |
636fae7ce6 | ||
![]() |
8197a26c49 | ||
![]() |
ba3b213423 | ||
![]() |
0be57a4e70 | ||
![]() |
80163d3fe2 | ||
![]() |
c8795b15f3 | ||
![]() |
e8c0ea5c94 | ||
![]() |
38ad6084bb | ||
![]() |
c726187b4d | ||
![]() |
3eafa2e13f | ||
![]() |
d13f096d4f | ||
![]() |
e2e37e1f01 | ||
![]() |
3bbe309de3 | ||
![]() |
2be7c787dd | ||
![]() |
9aecda4752 | ||
![]() |
9532075bc4 | ||
![]() |
5996b4d483 | ||
![]() |
fdd6fc18e1 | ||
![]() |
af3866fafe | ||
![]() |
53daaa9947 | ||
![]() |
3fed9e0b6a | ||
![]() |
f3168542fd | ||
![]() |
d266485fef | ||
![]() |
8738becd82 | ||
![]() |
ad43ca6629 | ||
![]() |
9368878963 | ||
![]() |
496491a43a | ||
![]() |
7494b39abc | ||
![]() |
74426a75f8 | ||
![]() |
9bac88a8cc | ||
![]() |
c0af53155c | ||
![]() |
e0aa6a4d0e | ||
![]() |
2425b674f7 | ||
![]() |
b7a1462ec6 | ||
![]() |
a31d857a6e | ||
![]() |
dbeb64c0dc | ||
![]() |
229c219751 | ||
![]() |
3216ffe42c | ||
![]() |
e2e3e5814e | ||
![]() |
5f709eb71e | ||
![]() |
d5bf36a85c | ||
![]() |
90d48c0c52 | ||
![]() |
62707aa86c | ||
![]() |
ac187a1480 | ||
![]() |
7b0bf7494f | ||
![]() |
c64219e249 | ||
![]() |
2127dd7fcb | ||
![]() |
2a583b94dc | ||
![]() |
147d9946c3 | ||
![]() |
993cfaeaff | ||
![]() |
3e70283221 | ||
![]() |
0697acb940 | ||
![]() |
8ca4d03649 | ||
![]() |
7a465ff532 | ||
![]() |
96dce86678 | ||
![]() |
8dd827f70d | ||
![]() |
572f0cd19d | ||
![]() |
047f243758 | ||
![]() |
5c494896c6 | ||
![]() |
b7e717afbc | ||
![]() |
2f3912582a | ||
![]() |
f7b9a54a71 | ||
![]() |
4e554083b0 | ||
![]() |
69b6490534 | ||
![]() |
8b336f6f9b | ||
![]() |
ef5868d412 | ||
![]() |
ce532bbb4d | ||
![]() |
66999ca9bb | ||
![]() |
65d0a6fe4b | ||
![]() |
f7724db62a | ||
![]() |
01c20651a4 | ||
![]() |
57d38ba893 |
55
.github/workflows/db-lint.yml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Linting rules on database schema.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'initdb/**'
|
||||
branches:
|
||||
- 'main'
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'initdb/**'
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
schemalint:
|
||||
name: schemalint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the source
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Pull Docker images
|
||||
run: docker-compose pull db api
|
||||
|
||||
- name: Run PostgSail Database & schemalint
|
||||
# Environment variables
|
||||
env:
|
||||
# The hostname used to communicate with the PostgreSQL service container
|
||||
PGHOST: localhost
|
||||
PGPORT: 5432
|
||||
PGDATABASE: signalk
|
||||
PGUSER: username
|
||||
PGPASSWORD: password
|
||||
run: |
|
||||
set -eu
|
||||
source .env
|
||||
docker-compose stop || true
|
||||
docker-compose rm || true
|
||||
docker-compose up -d db && sleep 30 && docker-compose up -d api && sleep 5
|
||||
docker-compose ps -a
|
||||
echo ${PGSAIL_API_URL}
|
||||
curl ${PGSAIL_API_URL}
|
||||
npm i -D schemalint
|
||||
npx schemalint
|
||||
- name: Show the logs
|
||||
if: always()
|
||||
run: |
|
||||
docker-compose logs
|
2
.github/workflows/db-test.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
source .env
|
||||
docker-compose stop || true
|
||||
docker-compose rm || true
|
||||
docker-compose up -d db && sleep 15 && docker-compose up -d api && sleep 5
|
||||
docker-compose up -d db && sleep 30 && docker-compose up -d api && sleep 5
|
||||
docker-compose ps -a
|
||||
echo ${PGSAIL_API_URL}
|
||||
curl ${PGSAIL_API_URL}
|
||||
|
2
.github/workflows/frontend-test.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
source .env
|
||||
docker-compose stop || true
|
||||
docker-compose rm || true
|
||||
docker-compose up -d db && sleep 15 && docker-compose up -d api && sleep 5
|
||||
docker-compose up -d db && sleep 30 && docker-compose up -d api && sleep 5
|
||||
docker-compose ps -a
|
||||
echo "Test PostgSail Web Unit Test"
|
||||
docker compose -f docker-compose.dev.yml -f docker-compose.yml up -d web_dev && sleep 100
|
||||
|
2
.github/workflows/grafana-test.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
source .env
|
||||
docker-compose stop || true
|
||||
docker-compose rm || true
|
||||
docker-compose up -d db && sleep 15
|
||||
docker-compose up -d db && sleep 30
|
||||
docker-compose ps -a
|
||||
echo "Test PostgSail Grafana Unit Test"
|
||||
docker-compose up -d app && sleep 5
|
||||
|
8
.gitignore
vendored
@@ -1,2 +1,10 @@
|
||||
.DS_Store
|
||||
.env
|
||||
initdb/*.csv
|
||||
initdb/*.no
|
||||
initdb/*.jwk
|
||||
tests/node_modules/
|
||||
tests/output/
|
||||
assets/*
|
||||
.pnpm-store/
|
||||
db-data/
|
22
.schemalintrc.js
Normal file
@@ -0,0 +1,22 @@
|
||||
module.exports = {
|
||||
connection: {
|
||||
host: process.env.PGHOST,
|
||||
user: process.env.PGUSER,
|
||||
password: process.env.PGPASSWORD,
|
||||
database: process.env.PGDATABASE,
|
||||
charset: "utf8",
|
||||
},
|
||||
|
||||
rules: {
|
||||
"name-casing": ["error", "snake"],
|
||||
"prefer-jsonb-to-json": ["error"],
|
||||
"prefer-text-to-varchar": ["error"],
|
||||
"prefer-timestamptz-to-timestamp": ["error"],
|
||||
"prefer-identity-to-serial": ["error"],
|
||||
//"name-inflection": ["error", "singular"],
|
||||
},
|
||||
|
||||
schemas: [{ name: "public" }, { name: "api" },{ name: "auth" }],
|
||||
|
||||
ignores: [],
|
||||
};
|
@@ -1,35 +0,0 @@
|
||||
# PostgSail ERD
|
||||
The Entity-Relationship Diagram (ERD) provides a graphical representation of database tables, columns, and inter-relationships. ERD can give sufficient information for the database administrator to follow when developing and maintaining the database.
|
||||
|
||||
## A global overview
|
||||

|
||||
|
||||
## Further
|
||||
There is 3 main schemas:
|
||||
- API Schema ERD
|
||||
- tables
|
||||
- metrics
|
||||
- logbook
|
||||
- ...
|
||||
- functions
|
||||
- ...
|
||||

|
||||
|
||||
- Auth Schema ERD
|
||||
- tables
|
||||
- accounts
|
||||
- vessels
|
||||
- ...
|
||||
- functions
|
||||
- ...
|
||||

|
||||
|
||||
- Public Schema ERD
|
||||
- tables
|
||||
- app_settings
|
||||
- tpl_messages
|
||||
- ...
|
||||
- functions
|
||||
- ...
|
||||

|
||||
|
25
README.md
@@ -3,8 +3,9 @@
|
||||
Effortless cloud based solution for storing and sharing your SignalK data. Allow you to effortlessly log your sails and monitor your boat with historical data.
|
||||
|
||||
[](https://github.com/xbgmsharp/postgsail/releases/latest)
|
||||
[](#license)
|
||||
[](#license)
|
||||
[](https://github.com/xbgmsharp/postgsail/issues)
|
||||
[](http://makeapullrequest.com)
|
||||
|
||||
[](https://github.com/xbgmsharp/postgsail/actions/workflows/db-test.yml)
|
||||
[](https://github.com/xbgmsharp/postgsail/actions/workflows/frontend-test.yml)
|
||||
@@ -19,22 +20,24 @@ postgsail-frontend:
|
||||
postgsail-telegram-bot:
|
||||
[](https://github.com/xbgmsharp/postgsail-telegram-bot/releases/latest)
|
||||
|
||||
[](https://www.bestpractices.dev/projects/8124)
|
||||
|
||||
## Features
|
||||
|
||||
- Automatically log your voyages without manually starting or stopping a trip.
|
||||
- Automatically capture the details of your voyages (boat speed, heading, wind speed, etc).
|
||||
- Timelapse video your trips!
|
||||
- Timelapse video your trips, with or without time control.
|
||||
- Add custom notes to your logs.
|
||||
- Export to CSV or GPX and download your logs.
|
||||
- Export to CSV, GPX, GeoJSON, KML and download your logs.
|
||||
- Aggregate your trip statistics: Longest voyage, time spent at anchorages, home ports etc.
|
||||
- See your moorages on a global map, with incoming and outgoing voyages from each trip.
|
||||
- Monitor your boat (position, depth, wind, temperature, battery charge status, etc.) remotely.
|
||||
- History: view trends.
|
||||
- Alert monitoring: get notification on low voltage or low fuel remotely.
|
||||
- Notification via email or PushOver, Telegram
|
||||
- Offline mode
|
||||
- Low Bandwidth mode
|
||||
- Awesome statistics and graphs
|
||||
- Notification via email or PushOver, Telegram.
|
||||
- Offline mode.
|
||||
- Low Bandwidth mode.
|
||||
- Awesome statistics and graphs.
|
||||
- Anything missing? just ask!
|
||||
|
||||
## Context
|
||||
@@ -144,7 +147,7 @@ Next, to ingest data from signalk, you need to install [signalk-postgsail](https
|
||||
Also, if you like, you can import saillogger data using the postgsail helpers, [postgsail-helpers](https://github.com/xbgmsharp/postgsail-helpers).
|
||||
|
||||
You might want to import your influxdb1 data as well, [outflux](https://github.com/timescale/outflux).
|
||||
Any taker on influxdb2 to PostgSail? It is definitely possible.
|
||||
For InfluxDB 2.x and 3.x. You will need to enable the 1.x APIs to use them. Consult the InfluxDB documentation for more details.
|
||||
|
||||
Last, if you like, you can import the sample data from Signalk NMEA Plaka by running the tests.
|
||||
If everything goes well all tests pass successfully and you should receive a few notifications by email or PushOver or Telegram.
|
||||
@@ -209,10 +212,6 @@ Out of the box iot platform using docker with the following software:
|
||||
- [PostGIS, a spatial database extender for PostgreSQL object-relational database.](https://postgis.net/)
|
||||
- [Grafana, open observability platform | Grafana Labs](https://grafana.com)
|
||||
|
||||
### Releases & updates
|
||||
|
||||
PostgSail Release Notes & Future Plans: see planned and in-progress updates and detailed information about current and past releases. [PostgSail project](https://github.com/xbgmsharp?tab=projects)
|
||||
|
||||
### Support
|
||||
|
||||
To get support, please create new [issue](https://github.com/xbgmsharp/postgsail/issues).
|
||||
@@ -226,4 +225,4 @@ Feel free to contribute.
|
||||
|
||||
### License
|
||||
|
||||
This script is free software, Apache License Version 2.0.
|
||||
This is a free software, Apache License Version 2.0.
|
||||
|
@@ -45,8 +45,12 @@ services:
|
||||
PGRST_DB_ANON_ROLE: api_anonymous
|
||||
PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000
|
||||
PGRST_DB_PRE_REQUEST: public.check_jwt
|
||||
PGRST_DB_POOL: 20
|
||||
PGRST_DB_URI: ${PGRST_DB_URI}
|
||||
PGRST_JWT_SECRET: ${PGRST_JWT_SECRET}
|
||||
PGRST_SERVER_TIMING_ENABLED: 1
|
||||
PGRST_DB_MAX_ROWS: 500
|
||||
PGRST_JWT_CACHE_MAX_LIFETIME: 3600
|
||||
depends_on:
|
||||
- db
|
||||
logging:
|
||||
@@ -74,10 +78,9 @@ services:
|
||||
env_file: .env
|
||||
environment:
|
||||
- GF_INSTALL_PLUGINS=pr0ps-trackmap-panel,fatcloud-windrose-panel
|
||||
- GF_SECURITY_ADMIN_PASSWORD=${PGSAIL_GRAFANA_PASSWORD}
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_SMTP_ENABLED=false
|
||||
- PGSAIL_GRAFANA_URI=db:5432
|
||||
- PGSAIL_GRAFANA_PASSWORD=${PGSAIL_GRAFANA_PASSWORD}
|
||||
depends_on:
|
||||
- db
|
||||
logging:
|
||||
|
34
docs/ERD/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# PostgSail ERD
|
||||
The Entity-Relationship Diagram (ERD) provides a graphical representation of database tables, columns, and inter-relationships. ERD can give sufficient information for the database administrator to follow when developing and maintaining the database.
|
||||
|
||||
## A global overview
|
||||
Auto generated Mermaid diagram using [mermerd](https://github.com/KarnerTh/mermerd) and [MermaidJs](https://github.com/mermaid-js/mermaid).
|
||||
|
||||
[PostgSail SQL Schema](https://github.com/xbgmsharp/postgsail/tree/main/docs/ERD/postgsail.md "PostgSail SQL Schema")
|
||||
|
||||
## Further
|
||||
There is 3 main schemas:
|
||||
- API Schema:
|
||||
- tables
|
||||
- metrics
|
||||
- logbook
|
||||
- ...
|
||||
- functions
|
||||
- ...
|
||||
|
||||
- Auth Schema:
|
||||
- tables
|
||||
- accounts
|
||||
- vessels
|
||||
- ...
|
||||
- functions
|
||||
- ...
|
||||
|
||||
- Public Schema:
|
||||
- tables
|
||||
- app_settings
|
||||
- tpl_messages
|
||||
- ...
|
||||
- functions
|
||||
- ...
|
||||
|
35
docs/ERD/mermerdConfig.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
# Connection properties
|
||||
connectionString: ${PGSAIL_DB_URI}
|
||||
|
||||
# Define what schemas should be used
|
||||
#useAllSchemas: true
|
||||
# or
|
||||
schema:
|
||||
- "public"
|
||||
- "api"
|
||||
- "auth"
|
||||
|
||||
# Define what tables should be used
|
||||
useAllTables: true
|
||||
# or
|
||||
#selectedTables:
|
||||
# - city
|
||||
# - customer
|
||||
|
||||
# Additional flags
|
||||
showAllConstraints: true
|
||||
encloseWithMermaidBackticks: true
|
||||
outputFileName: "postgsail.md"
|
||||
debug: true
|
||||
omitConstraintLabels: true
|
||||
omitAttributeKeys: true
|
||||
showDescriptions:
|
||||
- enumValues
|
||||
- columnComments
|
||||
- notNull
|
||||
showSchemaPrefix: true
|
||||
schemaPrefixSeparator: "_"
|
||||
|
||||
# Names must match the pattern <schema><schema_prefix><table>
|
||||
#relationshipLabels:
|
||||
# - "public_table public_another-table : label"
|
261
docs/ERD/postgsail.md
Normal file
@@ -0,0 +1,261 @@
|
||||
```mermaid
|
||||
erDiagram
|
||||
api_logbook {
|
||||
text _from
|
||||
double_precision _from_lat
|
||||
double_precision _from_lng
|
||||
integer _from_moorage_id "Link api.moorages with api.logbook via FOREIGN KEY and REFERENCES"
|
||||
timestamp_with_time_zone _from_time "{NOT_NULL}"
|
||||
text _to
|
||||
double_precision _to_lat
|
||||
double_precision _to_lng
|
||||
integer _to_moorage_id "Link api.moorages with api.logbook via FOREIGN KEY and REFERENCES"
|
||||
timestamp_with_time_zone _to_time
|
||||
boolean active
|
||||
double_precision avg_speed
|
||||
numeric distance "in NM"
|
||||
interval duration "Best to use standard ISO 8601"
|
||||
jsonb extra "computed signalk metrics of interest, runTime, currentLevel, etc"
|
||||
integer id "{NOT_NULL}"
|
||||
double_precision max_speed
|
||||
double_precision max_wind_speed
|
||||
text name
|
||||
text notes
|
||||
geography track_geog "postgis geography type default SRID 4326 Unit: degres"
|
||||
jsonb track_geojson "store generated geojson with track metrics data using with LineString and Point features, we can not depend api.metrics table"
|
||||
geometry track_geom "postgis geometry type EPSG:4326 Unit: degres"
|
||||
text vessel_id "{NOT_NULL}"
|
||||
}
|
||||
|
||||
api_metadata {
|
||||
boolean active "trigger monitor online/offline"
|
||||
boolean active
|
||||
double_precision beam
|
||||
text client_id
|
||||
text configuration
|
||||
timestamp_with_time_zone created_at "{NOT_NULL}"
|
||||
double_precision height
|
||||
integer id "{NOT_NULL}"
|
||||
double_precision length
|
||||
numeric mmsi
|
||||
text name
|
||||
text platform
|
||||
text plugin_version "{NOT_NULL}"
|
||||
numeric ship_type
|
||||
text signalk_version "{NOT_NULL}"
|
||||
timestamp_with_time_zone time "{NOT_NULL}"
|
||||
timestamp_with_time_zone updated_at "{NOT_NULL}"
|
||||
text vessel_id "Link auth.vessels with api.metadata via FOREIGN KEY and REFERENCES {NOT_NULL}"
|
||||
text vessel_id "{NOT_NULL}"
|
||||
}
|
||||
|
||||
api_metrics {
|
||||
double_precision anglespeedapparent
|
||||
text client_id
|
||||
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"
|
||||
jsonb metrics
|
||||
double_precision speedoverground
|
||||
text status
|
||||
timestamp_with_time_zone time "{NOT_NULL}"
|
||||
text vessel_id "{NOT_NULL}"
|
||||
double_precision windspeedapparent
|
||||
}
|
||||
|
||||
api_moorages {
|
||||
text country
|
||||
geography geog "postgis geography type default SRID 4326 Unit: degres"
|
||||
boolean home_flag
|
||||
integer id "{NOT_NULL}"
|
||||
double_precision latitude
|
||||
double_precision longitude
|
||||
text name
|
||||
jsonb nominatim
|
||||
text notes
|
||||
jsonb overpass
|
||||
integer reference_count
|
||||
integer stay_code "Link api.stays_at with api.moorages via FOREIGN KEY and REFERENCES"
|
||||
interval stay_duration "Best to use standard ISO 8601"
|
||||
text vessel_id "{NOT_NULL}"
|
||||
}
|
||||
|
||||
api_stays {
|
||||
boolean active
|
||||
timestamp_with_time_zone arrived "{NOT_NULL}"
|
||||
timestamp_with_time_zone departed
|
||||
interval duration "Best to use standard ISO 8601"
|
||||
geography geog "postgis geography type default SRID 4326 Unit: degres"
|
||||
integer id "{NOT_NULL}"
|
||||
double_precision latitude
|
||||
double_precision longitude
|
||||
integer moorage_id "Link api.moorages with api.stays via FOREIGN KEY and REFERENCES"
|
||||
text name
|
||||
text notes
|
||||
integer stay_code "Link api.stays_at with api.stays via FOREIGN KEY and REFERENCES"
|
||||
text vessel_id "{NOT_NULL}"
|
||||
}
|
||||
|
||||
api_stays_at {
|
||||
text description "{NOT_NULL}"
|
||||
integer stay_code "{NOT_NULL}"
|
||||
}
|
||||
|
||||
auth_accounts {
|
||||
timestamp_with_time_zone connected_at "{NOT_NULL}"
|
||||
timestamp_with_time_zone created_at "{NOT_NULL}"
|
||||
citext email "{NOT_NULL}"
|
||||
text first "User first name with CONSTRAINT CHECK {NOT_NULL}"
|
||||
integer id "{NOT_NULL}"
|
||||
text last "User last name with CONSTRAINT CHECK {NOT_NULL}"
|
||||
text pass "{NOT_NULL}"
|
||||
jsonb preferences
|
||||
name role "{NOT_NULL}"
|
||||
timestamp_with_time_zone updated_at "{NOT_NULL}"
|
||||
text user_id "{NOT_NULL}"
|
||||
}
|
||||
|
||||
auth_otp {
|
||||
text otp_pass "{NOT_NULL}"
|
||||
timestamp_with_time_zone otp_timestamp
|
||||
smallint otp_tries "{NOT_NULL}"
|
||||
citext user_email "{NOT_NULL}"
|
||||
}
|
||||
|
||||
auth_users {
|
||||
timestamp_with_time_zone connected_at "{NOT_NULL}"
|
||||
timestamp_with_time_zone created_at "{NOT_NULL}"
|
||||
name email "{NOT_NULL}"
|
||||
text first "{NOT_NULL}"
|
||||
name id "{NOT_NULL}"
|
||||
text last "{NOT_NULL}"
|
||||
jsonb preferences
|
||||
name role "{NOT_NULL}"
|
||||
timestamp_with_time_zone updated_at "{NOT_NULL}"
|
||||
text user_id "{NOT_NULL}"
|
||||
}
|
||||
|
||||
auth_vessels {
|
||||
timestamp_with_time_zone created_at "{NOT_NULL}"
|
||||
numeric mmsi "MMSI can be optional but if present must be a valid one and unique but must be in numeric range between 100000000 and 800000000"
|
||||
text name "{NOT_NULL}"
|
||||
citext owner_email "{NOT_NULL}"
|
||||
name role "{NOT_NULL}"
|
||||
timestamp_with_time_zone updated_at "{NOT_NULL}"
|
||||
text vessel_id "{NOT_NULL}"
|
||||
}
|
||||
|
||||
public_aistypes {
|
||||
text description
|
||||
numeric id
|
||||
}
|
||||
|
||||
public_app_settings {
|
||||
text name "application settings name key {NOT_NULL}"
|
||||
text value "application settings value {NOT_NULL}"
|
||||
}
|
||||
|
||||
public_badges {
|
||||
text description
|
||||
text name
|
||||
}
|
||||
|
||||
public_email_templates {
|
||||
text email_content
|
||||
text email_subject
|
||||
text name
|
||||
text pushover_message
|
||||
text pushover_title
|
||||
}
|
||||
|
||||
public_geocoders {
|
||||
text name
|
||||
text reverse_url
|
||||
text url
|
||||
}
|
||||
|
||||
public_iso3166 {
|
||||
text alpha_2
|
||||
text alpha_3
|
||||
text country
|
||||
integer id
|
||||
}
|
||||
|
||||
public_mid {
|
||||
text country
|
||||
integer country_id
|
||||
numeric id
|
||||
}
|
||||
|
||||
public_ne_10m_geography_marine_polys {
|
||||
text changed
|
||||
text featurecla
|
||||
geometry geom
|
||||
integer gid "{NOT_NULL}"
|
||||
text label
|
||||
double_precision max_label
|
||||
double_precision min_label
|
||||
text name
|
||||
text name_ar
|
||||
text name_bn
|
||||
text name_de
|
||||
text name_el
|
||||
text name_en
|
||||
text name_es
|
||||
text name_fa
|
||||
text name_fr
|
||||
text name_he
|
||||
text name_hi
|
||||
text name_hu
|
||||
text name_id
|
||||
text name_it
|
||||
text name_ja
|
||||
text name_ko
|
||||
text name_nl
|
||||
text name_pl
|
||||
text name_pt
|
||||
text name_ru
|
||||
text name_sv
|
||||
text name_tr
|
||||
text name_uk
|
||||
text name_ur
|
||||
text name_vi
|
||||
text name_zh
|
||||
text name_zht
|
||||
text namealt
|
||||
bigint ne_id
|
||||
text note
|
||||
smallint scalerank
|
||||
text wikidataid
|
||||
}
|
||||
|
||||
public_process_queue {
|
||||
text channel "{NOT_NULL}"
|
||||
integer id "{NOT_NULL}"
|
||||
text payload "{NOT_NULL}"
|
||||
timestamp_with_time_zone processed
|
||||
text ref_id "either user_id or vessel_id {NOT_NULL}"
|
||||
timestamp_with_time_zone stored "{NOT_NULL}"
|
||||
}
|
||||
|
||||
public_spatial_ref_sys {
|
||||
character_varying auth_name
|
||||
integer auth_srid
|
||||
character_varying proj4text
|
||||
integer srid "{NOT_NULL}"
|
||||
character_varying srtext
|
||||
}
|
||||
|
||||
api_logbook }o--|| api_metadata : ""
|
||||
api_logbook }o--|| api_moorages : ""
|
||||
api_logbook }o--|| api_moorages : ""
|
||||
api_metadata }o--|| auth_vessels : ""
|
||||
api_metrics }o--|| api_metadata : ""
|
||||
api_moorages }o--|| api_metadata : ""
|
||||
api_stays }o--|| api_metadata : ""
|
||||
api_moorages }o--|| api_stays_at : ""
|
||||
api_stays }o--|| api_moorages : ""
|
||||
api_stays }o--|| api_stays_at : ""
|
||||
auth_otp |o--|| auth_accounts : ""
|
||||
auth_vessels |o--|| auth_accounts : ""
|
||||
```
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 360 KiB |
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 222 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 195 KiB After Width: | Height: | Size: 195 KiB |
2
docs/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
Simple and scalable architecture.
|
2
frontend
@@ -1,134 +0,0 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "OIttR1sVk"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 3,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "OIttR1sVk"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"type": "welcome"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "OIttR1sVk"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 12,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 3
|
||||
},
|
||||
"id": 3,
|
||||
"links": [],
|
||||
"options": {
|
||||
"folderId": 0,
|
||||
"maxItems": 30,
|
||||
"query": "",
|
||||
"showHeadings": true,
|
||||
"showRecentlyViewed": true,
|
||||
"showSearch": false,
|
||||
"showStarred": true,
|
||||
"tags": []
|
||||
},
|
||||
"pluginVersion": "9.4.3",
|
||||
"tags": [],
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "OIttR1sVk"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Dashboards",
|
||||
"type": "dashlist"
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"revision": 1,
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"hidden": true,
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
],
|
||||
"type": "timepicker"
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "Home",
|
||||
"version": 0,
|
||||
"weekStart": ""
|
||||
}
|
@@ -20,8 +20,21 @@
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 6,
|
||||
"links": [],
|
||||
"id": 1,
|
||||
"links": [
|
||||
{
|
||||
"asDropdown": false,
|
||||
"icon": "external link",
|
||||
"includeVars": true,
|
||||
"keepTime": false,
|
||||
"tags": [],
|
||||
"targetBlank": true,
|
||||
"title": "New link",
|
||||
"tooltip": "",
|
||||
"type": "dashboards",
|
||||
"url": ""
|
||||
}
|
||||
],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
@@ -83,7 +96,7 @@
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true
|
||||
},
|
||||
"pluginVersion": "9.5.1",
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -220,7 +233,7 @@
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.5.1",
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -346,7 +359,7 @@
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.5.1",
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -481,7 +494,7 @@
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.5.1",
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -591,7 +604,7 @@
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.5.1",
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -719,7 +732,7 @@
|
||||
},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.5.1",
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -766,50 +779,108 @@
|
||||
"type": "stat"
|
||||
},
|
||||
{
|
||||
"aliasColors": {
|
||||
"electrical.batteries.256.current.mean": "blue"
|
||||
},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "line+area"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "transparent",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": -1
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "amp"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "electrical.batteries.256.current.mean"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "blue",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 5
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 47,
|
||||
"legend": {
|
||||
"avg": true,
|
||||
"current": false,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"options": {
|
||||
"alertThreshold": true
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"mean",
|
||||
"max",
|
||||
"min"
|
||||
],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"percentage": false,
|
||||
"pluginVersion": "9.5.1",
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -835,7 +906,8 @@
|
||||
"measurement": "electrical.batteries.256.current",
|
||||
"orderByTime": "ASC",
|
||||
"policy": "default",
|
||||
"rawSql": "",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.batteries.House.current' as NUMERIC) as current FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}';\n",
|
||||
"refId": "A",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
@@ -872,56 +944,379 @@
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"thresholds": [
|
||||
{
|
||||
"$$hashKey": "object:8288",
|
||||
"colorMode": "critical",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": -1,
|
||||
"yaxis": "left"
|
||||
},
|
||||
{
|
||||
"$$hashKey": "object:8294",
|
||||
"colorMode": "ok",
|
||||
"fill": true,
|
||||
"line": true,
|
||||
"op": "gt",
|
||||
"value": 1,
|
||||
"yaxis": "left"
|
||||
}
|
||||
],
|
||||
"timeRegions": [],
|
||||
"title": "House Amps",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"$$hashKey": "object:8148",
|
||||
"format": "amp",
|
||||
"logBase": 1,
|
||||
"show": true
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 10,
|
||||
"x": 12,
|
||||
"y": 5
|
||||
},
|
||||
"id": 48,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"$$hashKey": "object:8149",
|
||||
"format": "short",
|
||||
"logBase": 1,
|
||||
"show": true
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.batteries.House.capacity.stateOfCharge' as NUMERIC) * 100 as stateOfCharge FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}';\n",
|
||||
"refId": "A",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
}
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false
|
||||
}
|
||||
"title": "System - Battery SOC (State of Charge)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "Volts",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "volt"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "current"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "blue",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "voltage"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "yellow",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "current"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "amp"
|
||||
},
|
||||
{
|
||||
"id": "custom.axisLabel",
|
||||
"value": "Amps"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 13
|
||||
},
|
||||
"id": 37,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"mean",
|
||||
"max",
|
||||
"min"
|
||||
],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$__interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"measurement": "electrical.batteries.256.voltage",
|
||||
"orderByTime": "ASC",
|
||||
"policy": "default",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.batteries.House.voltage' as NUMERIC) as voltage FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}';\n",
|
||||
"refId": "A",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
]
|
||||
],
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$__interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"hide": false,
|
||||
"measurement": "electrical.batteries.256.current",
|
||||
"orderByTime": "ASC",
|
||||
"policy": "default",
|
||||
"query": "SELECT mean(\"value\") FROM \"electrical.batteries.256.current\" WHERE $timeFilter GROUP BY time($__interval) fill(null)",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.batteries.House.current' as NUMERIC) as current FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}';\n",
|
||||
"refId": "B",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
]
|
||||
],
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"title": "Battery Voltage and Current",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"aliasColors": {
|
||||
@@ -938,9 +1333,9 @@
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"w": 10,
|
||||
"x": 12,
|
||||
"y": 5
|
||||
"y": 13
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 45,
|
||||
@@ -960,7 +1355,7 @@
|
||||
"alertThreshold": true
|
||||
},
|
||||
"percentage": false,
|
||||
"pluginVersion": "9.5.1",
|
||||
"pluginVersion": "10.1.0",
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
@@ -1042,180 +1437,6 @@
|
||||
"align": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {
|
||||
"electrical.batteries.256.current.mean": "blue",
|
||||
"electrical.batteries.256.voltage.mean": "yellow"
|
||||
},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"description": "",
|
||||
"fill": 1,
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 13
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 37,
|
||||
"legend": {
|
||||
"alignAsTable": false,
|
||||
"avg": true,
|
||||
"current": false,
|
||||
"max": true,
|
||||
"min": true,
|
||||
"rightSide": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": true
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 1,
|
||||
"nullPointMode": "null",
|
||||
"options": {
|
||||
"alertThreshold": true
|
||||
},
|
||||
"percentage": false,
|
||||
"pluginVersion": "9.5.1",
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [
|
||||
{
|
||||
"$$hashKey": "object:5017",
|
||||
"alias": "electrical.batteries.256.current.mean",
|
||||
"yaxis": 2
|
||||
}
|
||||
],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$__interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"measurement": "electrical.batteries.256.voltage",
|
||||
"orderByTime": "ASC",
|
||||
"policy": "default",
|
||||
"refId": "A",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
]
|
||||
],
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$__interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"hide": false,
|
||||
"measurement": "electrical.batteries.256.current",
|
||||
"orderByTime": "ASC",
|
||||
"policy": "default",
|
||||
"query": "SELECT mean(\"value\") FROM \"electrical.batteries.256.current\" WHERE $timeFilter GROUP BY time($__interval) fill(null)",
|
||||
"rawQuery": true,
|
||||
"refId": "B",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
]
|
||||
],
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeRegions": [],
|
||||
"title": "House Bank Voltage vs Current",
|
||||
"tooltip": {
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "individual"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"mode": "time",
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"$$hashKey": "object:4372",
|
||||
"format": "volt",
|
||||
"label": "Volts",
|
||||
"logBase": 1,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"$$hashKey": "object:4373",
|
||||
"format": "amp",
|
||||
"label": "Amps",
|
||||
"logBase": 1,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"aliasColors": {
|
||||
"From grid": "#1f78c1",
|
||||
@@ -1232,9 +1453,9 @@
|
||||
"fillGradient": 0,
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"w": 10,
|
||||
"x": 12,
|
||||
"y": 13
|
||||
"y": 21
|
||||
},
|
||||
"hiddenSeries": false,
|
||||
"id": 10,
|
||||
@@ -1258,7 +1479,7 @@
|
||||
},
|
||||
"paceLength": 10,
|
||||
"percentage": false,
|
||||
"pluginVersion": "9.5.1",
|
||||
"pluginVersion": "10.1.0",
|
||||
"pointradius": 2,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
@@ -1416,6 +1637,25 @@
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"definition": "SET vessel.id = '${__user.login}';\nSELECT rtrim(key, 'voltage') AS __text ,key AS __value FROM api.monitoring_view2 where key ILIKE 'electrical.batteries%voltage';",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "Batteries",
|
||||
"multi": false,
|
||||
"name": "batteries",
|
||||
"options": [],
|
||||
"query": "SET vessel.id = '${__user.login}';\nSELECT rtrim(key, 'voltage') AS __text ,key AS __value FROM api.monitoring_view2 where key ILIKE 'electrical.batteries%voltage';",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -1447,9 +1687,9 @@
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "",
|
||||
"timezone": "utc",
|
||||
"title": "Electrical System",
|
||||
"uid": "rk0FTiIMk",
|
||||
"version": 1,
|
||||
"uid": "pgsail_tpl_electrical",
|
||||
"version": 11,
|
||||
"weekStart": ""
|
||||
}
|
@@ -466,7 +466,7 @@
|
||||
"timepicker": {},
|
||||
"timezone": "utc",
|
||||
"title": "Logbook",
|
||||
"uid": "E_FUkx9nk",
|
||||
"uid": "pgsail_tpl_logbook",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
@@ -25,7 +25,7 @@
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 2,
|
||||
"id": 3,
|
||||
"links": [
|
||||
{
|
||||
"asDropdown": false,
|
||||
@@ -92,7 +92,7 @@
|
||||
"text": {},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.4.3",
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -198,7 +198,7 @@
|
||||
"text": {},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.4.3",
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -279,6 +279,7 @@
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
@@ -439,6 +440,7 @@
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
@@ -573,6 +575,7 @@
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
@@ -638,7 +641,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nwith config as (select set_config('vessel.id', '${boat}', false) ) select * from api.monitoring_view",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nselect * from api.monitoring_humidity;\n",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -679,11 +682,11 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"title": "Title",
|
||||
"title": "environment.%.humidity",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"refresh": "5m",
|
||||
"revision": 1,
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
@@ -729,7 +732,7 @@
|
||||
},
|
||||
"timezone": "utc",
|
||||
"title": "Monitor",
|
||||
"uid": "apqDcPjMz",
|
||||
"uid": "pgsail_tpl_monitor",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
@@ -1335,7 +1335,7 @@
|
||||
},
|
||||
"timezone": "",
|
||||
"title": "RPI System",
|
||||
"uid": "4kxYm6j7k",
|
||||
"uid": "pgsail_tpl_rpi",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
635
grafana/dashboards/tpl/Solar.json
Normal file
@@ -0,0 +1,635 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "grafana"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Solar energy",
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 5,
|
||||
"links": [
|
||||
{
|
||||
"asDropdown": false,
|
||||
"icon": "external link",
|
||||
"includeVars": true,
|
||||
"keepTime": false,
|
||||
"tags": [],
|
||||
"targetBlank": true,
|
||||
"title": "New link",
|
||||
"tooltip": "",
|
||||
"type": "dashboards",
|
||||
"url": ""
|
||||
}
|
||||
],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"decimals": 1,
|
||||
"mappings": [
|
||||
{
|
||||
"options": {
|
||||
"0": {
|
||||
"text": "Aus"
|
||||
}
|
||||
},
|
||||
"type": "value"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"match": "null",
|
||||
"result": {
|
||||
"text": "Aus"
|
||||
}
|
||||
},
|
||||
"type": "special"
|
||||
}
|
||||
],
|
||||
"max": 400,
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "semi-dark-red",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "#EAB839",
|
||||
"value": 32
|
||||
},
|
||||
{
|
||||
"color": "dark-green",
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"color": "semi-dark-green",
|
||||
"value": 100
|
||||
},
|
||||
{
|
||||
"color": "light-green",
|
||||
"value": 200
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "watt"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 6,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 46,
|
||||
"interval": "",
|
||||
"options": {
|
||||
"orientation": "auto",
|
||||
"reduceOptions": {
|
||||
"calcs": [
|
||||
"lastNotNull"
|
||||
],
|
||||
"fields": "",
|
||||
"values": false
|
||||
},
|
||||
"showThresholdLabels": false,
|
||||
"showThresholdMarkers": true,
|
||||
"text": {
|
||||
"valueSize": 48
|
||||
}
|
||||
},
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "Watt",
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$__interval"
|
||||
],
|
||||
"type": "time"
|
||||
}
|
||||
],
|
||||
"measurement": "solarcharger/Yield/Power",
|
||||
"orderByTime": "ASC",
|
||||
"policy": "default",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.solar.Main.panelPower' as NUMERIC) as panelPower FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}' LIMIT 1;\n",
|
||||
"refId": "Watt",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "last"
|
||||
}
|
||||
]
|
||||
],
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"transparent": true,
|
||||
"type": "gauge"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "smooth",
|
||||
"lineStyle": {
|
||||
"fill": "solid"
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"min": 0,
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "watt"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 6,
|
||||
"y": 0
|
||||
},
|
||||
"id": 48,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.solar.Main.panelPower' as NUMERIC) as panelPower FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}';\n",
|
||||
"refId": "A",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "panelPower",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"description": "",
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "Volts",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "volt"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "current"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "blue",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "panelvoltage"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "color",
|
||||
"value": {
|
||||
"fixedColor": "yellow",
|
||||
"mode": "fixed"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "current"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "unit",
|
||||
"value": "amp"
|
||||
},
|
||||
{
|
||||
"id": "custom.axisLabel",
|
||||
"value": "Amps"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 18,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 47,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [
|
||||
"mean",
|
||||
"max",
|
||||
"min"
|
||||
],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "multi",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "10.1.0",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$__interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"measurement": "electrical.batteries.256.voltage",
|
||||
"orderByTime": "ASC",
|
||||
"policy": "default",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.solar.Main.panelVoltage' as NUMERIC) as panelVoltage FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}';\n",
|
||||
"refId": "A",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
]
|
||||
],
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
},
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"groupBy": [
|
||||
{
|
||||
"params": [
|
||||
"$__interval"
|
||||
],
|
||||
"type": "time"
|
||||
},
|
||||
{
|
||||
"params": [
|
||||
"null"
|
||||
],
|
||||
"type": "fill"
|
||||
}
|
||||
],
|
||||
"hide": false,
|
||||
"measurement": "electrical.batteries.256.current",
|
||||
"orderByTime": "ASC",
|
||||
"policy": "default",
|
||||
"query": "SELECT mean(\"value\") FROM \"electrical.batteries.256.current\" WHERE $timeFilter GROUP BY time($__interval) fill(null)",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT m.time, cast(m.metrics->'electrical.batteries.House.current' as NUMERIC) as current FROM api.metrics m WHERE $__timeFilter(time) AND m.vessel_id = '${boat}';\n",
|
||||
"refId": "B",
|
||||
"resultFormat": "time_series",
|
||||
"select": [
|
||||
[
|
||||
{
|
||||
"params": [
|
||||
"value"
|
||||
],
|
||||
"type": "field"
|
||||
},
|
||||
{
|
||||
"params": [],
|
||||
"type": "mean"
|
||||
}
|
||||
]
|
||||
],
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"title": "panelVoltage",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "5m",
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"definition": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
|
||||
"description": "Vessel Name",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "Boat",
|
||||
"multi": false,
|
||||
"name": "boat",
|
||||
"options": [],
|
||||
"query": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"definition": "SET vessel.id = '${__user.login}';\nSELECT rtrim(key, 'panelVoltage') AS __text ,key AS __value FROM api.monitoring_view2 where key ILIKE 'electrical.solar%panelVoltage';",
|
||||
"description": "Solar Panel",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "solarPanel",
|
||||
"multi": false,
|
||||
"name": "solar_panel",
|
||||
"options": [],
|
||||
"query": "SET vessel.id = '${__user.login}';\nSELECT rtrim(key, 'panelVoltage') AS __text ,key AS __value FROM api.monitoring_view2 where key ILIKE 'electrical.solar%panelVoltage';",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-24h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
]
|
||||
},
|
||||
"timezone": "utc",
|
||||
"title": "Solar System",
|
||||
"uid": "pgsail_tpl_solar",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
@@ -1936,7 +1936,7 @@
|
||||
"yBucketBound": "auto"
|
||||
}
|
||||
],
|
||||
"refresh": "1m",
|
||||
"refresh": "5m",
|
||||
"schemaVersion": 37,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
@@ -1981,7 +1981,7 @@
|
||||
},
|
||||
"timezone": "utc",
|
||||
"title": "Weather",
|
||||
"uid": "631a97c2e",
|
||||
"uid": "pgsail_tpl_weather",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
297
grafana/dashboards/tpl/home.json
Normal file
@@ -0,0 +1,297 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 5,
|
||||
"links": [
|
||||
{
|
||||
"asDropdown": false,
|
||||
"icon": "external link",
|
||||
"includeVars": true,
|
||||
"keepTime": false,
|
||||
"tags": [],
|
||||
"targetBlank": true,
|
||||
"title": "New link",
|
||||
"tooltip": "",
|
||||
"type": "dashboards",
|
||||
"url": ""
|
||||
}
|
||||
],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "OIttR1sVk"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 10,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 3,
|
||||
"links": [],
|
||||
"options": {
|
||||
"folderId": 0,
|
||||
"includeVars": false,
|
||||
"keepTime": false,
|
||||
"maxItems": 30,
|
||||
"query": "",
|
||||
"showHeadings": true,
|
||||
"showRecentlyViewed": true,
|
||||
"showSearch": false,
|
||||
"showStarred": true,
|
||||
"tags": []
|
||||
},
|
||||
"pluginVersion": "10.1.4",
|
||||
"tags": [],
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "OIttR1sVk"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "PostgSail Dashboards",
|
||||
"type": "dashlist"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 12,
|
||||
"x": 10,
|
||||
"y": 0
|
||||
},
|
||||
"id": 5,
|
||||
"maxDataPoints": 500,
|
||||
"options": {
|
||||
"basemap": {
|
||||
"config": {},
|
||||
"name": "Layer 0",
|
||||
"type": "default"
|
||||
},
|
||||
"controls": {
|
||||
"mouseWheelZoom": true,
|
||||
"showAttribution": true,
|
||||
"showDebug": false,
|
||||
"showMeasure": false,
|
||||
"showScale": false,
|
||||
"showZoom": true
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"config": {
|
||||
"showLegend": true,
|
||||
"style": {
|
||||
"color": {
|
||||
"fixed": "dark-green"
|
||||
},
|
||||
"opacity": 0.4,
|
||||
"rotation": {
|
||||
"fixed": 0,
|
||||
"max": 360,
|
||||
"min": -360,
|
||||
"mode": "mod"
|
||||
},
|
||||
"size": {
|
||||
"fixed": 5,
|
||||
"max": 15,
|
||||
"min": 2
|
||||
},
|
||||
"symbol": {
|
||||
"fixed": "img/icons/marker/circle.svg",
|
||||
"mode": "fixed"
|
||||
},
|
||||
"textConfig": {
|
||||
"fontSize": 12,
|
||||
"offsetX": 0,
|
||||
"offsetY": 0,
|
||||
"textAlign": "center",
|
||||
"textBaseline": "middle"
|
||||
}
|
||||
}
|
||||
},
|
||||
"filterData": {
|
||||
"id": "byRefId",
|
||||
"options": "A"
|
||||
},
|
||||
"location": {
|
||||
"latitude": "value",
|
||||
"longitude": "value",
|
||||
"mode": "auto"
|
||||
},
|
||||
"name": "Boat",
|
||||
"tooltip": true,
|
||||
"type": "markers"
|
||||
}
|
||||
],
|
||||
"tooltip": {
|
||||
"mode": "details"
|
||||
},
|
||||
"view": {
|
||||
"allLayers": true,
|
||||
"id": "fit",
|
||||
"lat": 0,
|
||||
"lon": 0,
|
||||
"zoom": 5
|
||||
}
|
||||
},
|
||||
"pluginVersion": "10.1.4",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"editorMode": "code",
|
||||
"format": "table",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT latitude, longitude FROM api.metrics WHERE vessel_id = '${boat}' ORDER BY time DESC LIMIT 1;",
|
||||
"refId": "A",
|
||||
"sql": {
|
||||
"columns": [
|
||||
{
|
||||
"parameters": [],
|
||||
"type": "function"
|
||||
}
|
||||
],
|
||||
"groupBy": [
|
||||
{
|
||||
"property": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": "groupBy"
|
||||
}
|
||||
],
|
||||
"limit": 50
|
||||
}
|
||||
}
|
||||
],
|
||||
"title": "Location",
|
||||
"type": "geomap"
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"revision": 1,
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"definition": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
|
||||
"description": "Vessel Name",
|
||||
"hide": 0,
|
||||
"includeAll": false,
|
||||
"label": "Boat",
|
||||
"multi": false,
|
||||
"name": "boat",
|
||||
"options": [],
|
||||
"query": "SET \"user.email\" = '${__user.email}';\nSET vessel.id = '${__user.login}';\nSELECT\n v.name AS __text,\n m.vessel_id AS __value\n FROM auth.vessels v\n JOIN api.metadata m ON v.owner_email = '${__user.email}' and m.vessel_id = v.vessel_id;",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-90d",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"hidden": true,
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
],
|
||||
"type": "timepicker"
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "Home",
|
||||
"uid": "pgsail_tpl_home",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
@@ -3,14 +3,22 @@ allow_sign_up = false
|
||||
auto_assign_org = true
|
||||
auto_assign_org_role = Editor
|
||||
|
||||
[auth.proxy]
|
||||
enabled = true
|
||||
header_name = X-WEBAUTH-USER
|
||||
header_property = email
|
||||
auto_sign_up = true
|
||||
enable_login_token = true
|
||||
login_maximum_inactive_lifetime_duration = 12h
|
||||
login_maximum_lifetime_duration = 1d
|
||||
|
||||
[dashboards]
|
||||
default_home_dashboard_path = /etc/grafana/dashboards/home.json
|
||||
default_home_dashboard_path = /etc/grafana/dashboards/tpl/home.json
|
||||
min_refresh_interval = 1m
|
||||
|
||||
[alerting]
|
||||
enabled = false
|
||||
|
||||
[unified_alerting]
|
||||
enabled = false
|
||||
|
||||
[analytics]
|
||||
feedback_links_enabled = false
|
||||
reporting_enabled = false
|
||||
|
||||
[news]
|
||||
news_feed_enabled = false
|
||||
|
||||
[help]
|
||||
enabled = false
|
||||
|
@@ -20,6 +20,6 @@ providers:
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
# <string, required> path to dashboard files on disk. Required when using the 'file' type
|
||||
path: /etc/grafana/dashboards/
|
||||
path: /etc/grafana/dashboards/tpl/
|
||||
# <bool> use folder names from filesystem to create folders in Grafana
|
||||
foldersFromFilesStructure: true
|
||||
|
@@ -5,18 +5,18 @@
|
||||
-- https://groups.google.com/g/signalk/c/W2H15ODCic4
|
||||
--
|
||||
-- Description:
|
||||
-- Insert data into table metadata from API using PostgREST
|
||||
-- Insert data into table metrics from API using PostgREST
|
||||
-- TimescaleDB Hypertable to store signalk metrics
|
||||
-- pgsql functions to generate logbook, stays, moorages
|
||||
-- Insert data into table api.metadata from API using PostgREST
|
||||
-- Insert data into table api.metrics from API using PostgREST
|
||||
-- TimescaleDB Hypertable to store signalk metrics on table api.metrics
|
||||
-- pgsql functions to generate logbook, stays, moorages from table api.metrics
|
||||
-- CRON functions to process logbook, stays, moorages
|
||||
-- python functions for geo reverse and send notification via email and/or pushover
|
||||
-- python functions for geo reverse and send notification via email, pushover, telegram
|
||||
-- Views statistics, timelapse, monitoring, logs
|
||||
-- Always store time in UTC
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
-- vessels signalk -(POST)-> metadata -> metadata_upsert -(trigger)-> metadata_upsert_trigger_fn (INSERT or UPDATE)
|
||||
-- vessels signalk -(POST)-> metrics -> metrics -(trigger)-> metrics_fn new log,stay,moorage
|
||||
-- vessels signalk -(POST)-> metadata -> metadata_upsert_trigger -(BEFORE INSERT)-> metadata_upsert_trigger_fn (INSERT or UPDATE)
|
||||
-- vessels signalk -(POST)-> metrics -> metrics_trigger -(BEFORE INSERT)-> metrics_trigger_fn (INSERT or UPDATE new log,stay)
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
|
@@ -8,7 +8,7 @@
|
||||
---------------------------------------------------------------------------
|
||||
-- Metadata from signalk
|
||||
CREATE TABLE IF NOT EXISTS api.metadata(
|
||||
id SERIAL PRIMARY KEY,
|
||||
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
name TEXT NULL,
|
||||
mmsi NUMERIC NULL,
|
||||
client_id TEXT NULL,
|
||||
@@ -20,30 +20,29 @@ CREATE TABLE IF NOT EXISTS api.metadata(
|
||||
ship_type NUMERIC NULL,
|
||||
plugin_version TEXT NOT NULL,
|
||||
signalk_version TEXT NOT NULL,
|
||||
time TIMESTAMP WITHOUT TIME ZONE NOT NULL, -- should be rename to last_update !?
|
||||
time TIMESTAMPTZ NOT NULL, -- should be rename to last_update !?
|
||||
platform TEXT NULL,
|
||||
configuration TEXT NULL,
|
||||
active BOOLEAN DEFAULT True, -- trigger monitor online/offline
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
api.metadata
|
||||
IS 'Stores metadata from vessel';
|
||||
IS 'Stores metadata received from vessel, aka signalk plugin';
|
||||
COMMENT ON COLUMN api.metadata.active IS 'trigger monitor online/offline';
|
||||
-- Index
|
||||
CREATE INDEX metadata_vessel_id_idx ON api.metadata (vessel_id);
|
||||
--CREATE INDEX metadata_mmsi_idx ON api.metadata (mmsi);
|
||||
-- is unused index ?
|
||||
CREATE INDEX metadata_name_idx ON api.metadata (name);
|
||||
COMMENT ON COLUMN api.metadata.vessel_id IS 'vessel_id link auth.vessels with api.metadata';
|
||||
-- Duplicate Indexes
|
||||
--CREATE INDEX metadata_vessel_id_idx ON api.metadata (vessel_id);
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Metrics from signalk
|
||||
-- Create vessel status enum
|
||||
CREATE TYPE status AS ENUM ('sailing', 'motoring', 'moored', 'anchored');
|
||||
CREATE TYPE status_type AS ENUM ('sailing', 'motoring', 'moored', 'anchored');
|
||||
-- Table api.metrics
|
||||
CREATE TABLE IF NOT EXISTS api.metrics (
|
||||
time TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
--client_id VARCHAR(255) NOT NULL REFERENCES api.metadata(client_id) ON DELETE RESTRICT,
|
||||
time TIMESTAMPTZ NOT NULL,
|
||||
client_id TEXT NULL,
|
||||
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
|
||||
latitude DOUBLE PRECISION NULL,
|
||||
@@ -52,8 +51,8 @@ CREATE TABLE IF NOT EXISTS api.metrics (
|
||||
courseOverGroundTrue DOUBLE PRECISION NULL,
|
||||
windSpeedApparent DOUBLE PRECISION NULL,
|
||||
angleSpeedApparent DOUBLE PRECISION NULL,
|
||||
status status NULL,
|
||||
metrics jsonb NULL,
|
||||
status TEXT NULL,
|
||||
metrics JSONB NULL,
|
||||
--CONSTRAINT valid_client_id CHECK (length(client_id) > 10),
|
||||
--CONSTRAINT valid_latitude CHECK (latitude >= -90 and latitude <= 90),
|
||||
--CONSTRAINT valid_longitude CHECK (longitude >= -180 and longitude <= 180),
|
||||
@@ -96,25 +95,24 @@ SELECT create_hypertable('api.metrics', 'time', chunk_time_interval => INTERVAL
|
||||
-- Check unused index
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api.logbook(
|
||||
id SERIAL PRIMARY KEY,
|
||||
--client_id VARCHAR(255) NOT NULL REFERENCES api.metadata(client_id) ON DELETE RESTRICT,
|
||||
--client_id VARCHAR(255) NULL,
|
||||
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
|
||||
active BOOLEAN DEFAULT false,
|
||||
name VARCHAR(255),
|
||||
_from VARCHAR(255),
|
||||
name TEXT,
|
||||
_from_moorage_id INT NULL,
|
||||
_from TEXT,
|
||||
_from_lat DOUBLE PRECISION NULL,
|
||||
_from_lng DOUBLE PRECISION NULL,
|
||||
_to VARCHAR(255),
|
||||
_to_moorage_id INT NULL,
|
||||
_to TEXT,
|
||||
_to_lat DOUBLE PRECISION NULL,
|
||||
_to_lng DOUBLE PRECISION NULL,
|
||||
--track_geom Geometry(LINESTRING)
|
||||
track_geom geometry(LINESTRING,4326) NULL,
|
||||
track_geog geography(LINESTRING) NULL,
|
||||
track_geojson JSON NULL,
|
||||
track_gpx XML NULL,
|
||||
_from_time TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
_to_time TIMESTAMP WITHOUT TIME ZONE NULL,
|
||||
track_geojson JSONB NULL,
|
||||
_from_time TIMESTAMPTZ NOT NULL,
|
||||
_to_time TIMESTAMPTZ NULL,
|
||||
distance NUMERIC, -- meters?
|
||||
duration INTERVAL, -- duration in days and hours?
|
||||
avg_speed DOUBLE PRECISION NULL,
|
||||
@@ -129,32 +127,35 @@ COMMENT ON TABLE
|
||||
IS 'Stores generated logbook';
|
||||
COMMENT ON COLUMN api.logbook.distance IS 'in NM';
|
||||
COMMENT ON COLUMN api.logbook.extra IS 'computed signalk metrics of interest, runTime, currentLevel, etc';
|
||||
COMMENT ON COLUMN api.logbook.duration IS 'Best to use standard ISO 8601';
|
||||
|
||||
-- Index todo!
|
||||
CREATE INDEX logbook_vessel_id_idx ON api.logbook (vessel_id);
|
||||
CREATE INDEX logbook_from_time_idx ON api.logbook (_from_time);
|
||||
CREATE INDEX logbook_to_time_idx ON api.logbook (_to_time);
|
||||
CREATE INDEX logbook_from_moorage_id_idx ON api.logbook (_from_moorage_id);
|
||||
CREATE INDEX logbook_to_moorage_id_idx ON api.logbook (_to_moorage_id);
|
||||
CREATE INDEX ON api.logbook USING GIST ( track_geom );
|
||||
COMMENT ON COLUMN api.logbook.track_geom IS 'postgis geometry type EPSG:4326 Unit: degres';
|
||||
CREATE INDEX ON api.logbook USING GIST ( track_geog );
|
||||
COMMENT ON COLUMN api.logbook.track_geog IS 'postgis geography type default SRID 4326 Unit: degres';
|
||||
-- Otherwise -- ERROR: Only lon/lat coordinate systems are supported in geography.
|
||||
COMMENT ON COLUMN api.logbook.track_geojson IS 'store the geojson track metrics data, can not depend api.metrics table, should be generate from linetring to save disk space?';
|
||||
COMMENT ON COLUMN api.logbook.track_gpx IS 'store the gpx track metrics data, can not depend api.metrics table, should be generate from linetring to save disk space?';
|
||||
COMMENT ON COLUMN api.logbook.track_geojson IS 'store generated geojson with track metrics data using with LineString and Point features, we can not depend api.metrics table';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Stays
|
||||
-- virtual logbook by boat?
|
||||
CREATE TABLE IF NOT EXISTS api.stays(
|
||||
id SERIAL PRIMARY KEY,
|
||||
--client_id VARCHAR(255) NOT NULL REFERENCES api.metadata(client_id) ON DELETE RESTRICT,
|
||||
--client_id VARCHAR(255) NULL,
|
||||
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
|
||||
active BOOLEAN DEFAULT false,
|
||||
name VARCHAR(255),
|
||||
moorage_id INT NULL,
|
||||
name TEXT,
|
||||
latitude DOUBLE PRECISION NULL,
|
||||
longitude DOUBLE PRECISION NULL,
|
||||
geog GEOGRAPHY(POINT) NULL,
|
||||
arrived TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
departed TIMESTAMP WITHOUT TIME ZONE,
|
||||
arrived TIMESTAMPTZ NOT NULL,
|
||||
departed TIMESTAMPTZ,
|
||||
duration INTERVAL, -- duration in days and hours?
|
||||
stay_code INT DEFAULT 1, -- REFERENCES api.stays_at(stay_code),
|
||||
notes TEXT NULL
|
||||
@@ -163,9 +164,11 @@ CREATE TABLE IF NOT EXISTS api.stays(
|
||||
COMMENT ON TABLE
|
||||
api.stays
|
||||
IS 'Stores generated stays';
|
||||
COMMENT ON COLUMN api.stays.duration IS 'Best to use standard ISO 8601';
|
||||
|
||||
-- Index
|
||||
CREATE INDEX stays_vessel_id_idx ON api.stays (vessel_id);
|
||||
CREATE INDEX stays_moorage_id_idx ON api.stays (moorage_id);
|
||||
CREATE INDEX ON api.stays USING GIST ( geog );
|
||||
COMMENT ON COLUMN api.stays.geog IS 'postgis geography type default SRID 4326 Unit: degres';
|
||||
-- With other SRID ERROR: Only lon/lat coordinate systems are supported in geography.
|
||||
@@ -174,13 +177,10 @@ COMMENT ON COLUMN api.stays.geog IS 'postgis geography type default SRID 4326 Un
|
||||
-- Moorages
|
||||
-- virtual logbook by boat?
|
||||
CREATE TABLE IF NOT EXISTS api.moorages(
|
||||
id SERIAL PRIMARY KEY,
|
||||
--client_id VARCHAR(255) NOT NULL REFERENCES api.metadata(client_id) ON DELETE RESTRICT,
|
||||
--client_id VARCHAR(255) NULL,
|
||||
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
|
||||
name TEXT,
|
||||
country TEXT,
|
||||
stay_id INT NOT NULL, -- needed?
|
||||
stay_code INT DEFAULT 1, -- needed? REFERENCES api.stays_at(stay_code)
|
||||
stay_duration INTERVAL NULL,
|
||||
reference_count INT DEFAULT 1,
|
||||
@@ -188,7 +188,9 @@ CREATE TABLE IF NOT EXISTS api.moorages(
|
||||
longitude DOUBLE PRECISION NULL,
|
||||
geog GEOGRAPHY(POINT) NULL,
|
||||
home_flag BOOLEAN DEFAULT false,
|
||||
notes TEXT NULL
|
||||
notes TEXT NULL,
|
||||
overpass JSONB NULL,
|
||||
nominatim JSONB NULL
|
||||
);
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
@@ -200,11 +202,12 @@ CREATE INDEX moorages_vessel_id_idx ON api.moorages (vessel_id);
|
||||
CREATE INDEX ON api.moorages USING GIST ( geog );
|
||||
COMMENT ON COLUMN api.moorages.geog IS 'postgis geography type default SRID 4326 Unit: degres';
|
||||
-- With other SRID ERROR: Only lon/lat coordinate systems are supported in geography.
|
||||
COMMENT ON COLUMN api.moorages.stay_duration IS 'Best to use standard ISO 8601';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Stay Type
|
||||
CREATE TABLE IF NOT EXISTS api.stays_at(
|
||||
stay_code INTEGER NOT NULL,
|
||||
stay_code INTEGER UNIQUE NOT NULL,
|
||||
description TEXT NOT NULL
|
||||
);
|
||||
-- Description
|
||||
@@ -255,7 +258,10 @@ CREATE FUNCTION metadata_upsert_trigger_fn() RETURNS trigger AS $metadata_upsert
|
||||
ship_type = NEW.ship_type,
|
||||
plugin_version = NEW.plugin_version,
|
||||
signalk_version = NEW.signalk_version,
|
||||
time = NEW.time,
|
||||
platform = NEW.platform,
|
||||
configuration = NEW.configuration,
|
||||
-- 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
|
||||
active = true
|
||||
WHERE id = metadata_id;
|
||||
RETURN NULL; -- Ignore insert
|
||||
@@ -264,7 +270,9 @@ CREATE FUNCTION metadata_upsert_trigger_fn() RETURNS trigger AS $metadata_upsert
|
||||
-- set vessel_id from jwt if not present in INSERT query
|
||||
NEW.vessel_id := current_setting('vessel.id');
|
||||
END IF;
|
||||
-- Insert new vessel metadata and
|
||||
-- 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;
|
||||
@@ -299,6 +307,22 @@ COMMENT ON FUNCTION
|
||||
public.metadata_notification_trigger_fn
|
||||
IS 'process metadata notification from vessel, monitoring_online';
|
||||
|
||||
-- FUNCTION Metadata grafana provisioning for new vessel after insert
|
||||
DROP FUNCTION IF EXISTS metadata_grafana_trigger_fn;
|
||||
CREATE FUNCTION metadata_grafana_trigger_fn() RETURNS trigger AS $metadata_grafana$
|
||||
DECLARE
|
||||
BEGIN
|
||||
RAISE NOTICE 'metadata_grafana_trigger_fn [%]', NEW;
|
||||
INSERT INTO process_queue (channel, payload, stored, ref_id)
|
||||
VALUES ('grafana', NEW.id, now(), NEW.vessel_id);
|
||||
RETURN NULL;
|
||||
END;
|
||||
$metadata_grafana$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.metadata_grafana_trigger_fn
|
||||
IS 'process metadata grafana provisioning from vessel';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Trigger metadata table
|
||||
--
|
||||
@@ -316,7 +340,15 @@ CREATE TRIGGER metadata_notification_trigger AFTER INSERT ON api.metadata
|
||||
-- Description
|
||||
COMMENT ON TRIGGER
|
||||
metadata_notification_trigger ON api.metadata
|
||||
IS 'AFTER INSERT ON api.metadata run function metadata_update_trigger_fn for notification on new vessel';
|
||||
IS 'AFTER INSERT ON api.metadata run function metadata_notification_trigger_fn for later notification on new vessel';
|
||||
|
||||
-- Metadata trigger AFTER INSERT
|
||||
CREATE TRIGGER metadata_grafana_trigger AFTER INSERT ON api.metadata
|
||||
FOR EACH ROW EXECUTE FUNCTION metadata_grafana_trigger_fn();
|
||||
-- Description
|
||||
COMMENT ON TRIGGER
|
||||
metadata_grafana_trigger ON api.metadata
|
||||
IS 'AFTER INSERT ON api.metadata run function metadata_grafana_trigger_fn for later grafana provisioning on new vessel';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Trigger Functions metrics table
|
||||
@@ -327,13 +359,13 @@ COMMENT ON TRIGGER
|
||||
DROP FUNCTION IF EXISTS metrics_trigger_fn;
|
||||
CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
|
||||
DECLARE
|
||||
previous_status varchar;
|
||||
previous_time TIMESTAMP WITHOUT TIME ZONE;
|
||||
stay_code integer;
|
||||
logbook_id integer;
|
||||
stay_id integer;
|
||||
valid_status BOOLEAN;
|
||||
previous_metric record;
|
||||
stay_code INTEGER;
|
||||
logbook_id INTEGER;
|
||||
stay_id INTEGER;
|
||||
valid_status BOOLEAN := False;
|
||||
_vessel_id TEXT;
|
||||
distance BOOLEAN := False;
|
||||
BEGIN
|
||||
--RAISE NOTICE 'metrics_trigger_fn';
|
||||
--RAISE WARNING 'metrics_trigger_fn [%] [%]', current_setting('vessel.id', true), NEW;
|
||||
@@ -344,20 +376,20 @@ CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
|
||||
END IF;
|
||||
-- Boat metadata are check using api.metrics REFERENCES to api.metadata
|
||||
-- Fetch the latest entry to compare status against the new status to be insert
|
||||
SELECT coalesce(m.status, 'moored'), m.time INTO previous_status, previous_time
|
||||
SELECT * INTO previous_metric
|
||||
FROM api.metrics m
|
||||
WHERE m.vessel_id IS NOT NULL
|
||||
AND m.vessel_id = current_setting('vessel.id', true)
|
||||
ORDER BY m.time DESC LIMIT 1;
|
||||
--RAISE NOTICE 'Metrics Status, New:[%] Previous:[%]', NEW.status, previous_status;
|
||||
IF previous_time = NEW.time THEN
|
||||
--RAISE NOTICE 'Metrics Status, New:[%] Previous:[%]', NEW.status, previous_metric.status;
|
||||
IF previous_metric.time = NEW.time THEN
|
||||
-- Ignore entry if same time
|
||||
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], duplicate time [%] = [%]', NEW.vessel_id, previous_time, NEW.time;
|
||||
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], duplicate time [%] = [%]', NEW.vessel_id, previous_metric.time, NEW.time;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
IF previous_time > NEW.time THEN
|
||||
IF previous_metric.time > NEW.time THEN
|
||||
-- Ignore entry if new time is later than previous time
|
||||
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], new time is older than previous_time [%] > [%]', NEW.vessel_id, previous_time, NEW.time;
|
||||
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], new time is older than previous_metric.time [%] > [%]', NEW.vessel_id, previous_metric.time, NEW.time;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
-- Check if latitude or longitude are type double
|
||||
@@ -384,18 +416,36 @@ CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
|
||||
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], invalid longitude >= 180 OR <= -180 [%] [%]', NEW.vessel_id, NEW.latitude, NEW.longitude;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
-- Check if status is null
|
||||
IF NEW.status IS NULL THEN
|
||||
-- Check if valid longitude and latitude not close to -0.0000001 from Victron Cerbo
|
||||
IF NEW.latitude = NEW.longitude THEN
|
||||
-- Ignore entry if latitude,longitude are equal
|
||||
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], latitude and longitude are equal [%] [%]', NEW.vessel_id, NEW.latitude, NEW.longitude;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
-- Check distance with previous point is > 10km
|
||||
--SELECT ST_Distance(
|
||||
-- ST_MakePoint(NEW.latitude,NEW.longitude)::geography,
|
||||
-- ST_MakePoint(previous_metric.latitude,previous_metric.longitude)::geography) > 10000 INTO distance;
|
||||
--IF distance IS True THEN
|
||||
-- RAISE WARNING 'Metrics Ignoring metric, distance between previous metric and new metric is too large, vessel_id [%] distance[%]', NEW.vessel_id, distance;
|
||||
-- RETURN NULL;
|
||||
--END IF;
|
||||
-- Check if status is null but speed is over 3knots set status to sailing
|
||||
IF NEW.status IS NULL AND NEW.speedoverground >= 3 THEN
|
||||
RAISE WARNING 'Metrics Unknown NEW.status, vessel_id [%], null status, set to sailing because of speedoverground is +3 from [%]', NEW.vessel_id, NEW.status;
|
||||
NEW.status := 'sailing';
|
||||
-- Check if status is null then set status to default moored
|
||||
ELSIF NEW.status IS NULL THEN
|
||||
RAISE WARNING 'Metrics Unknown NEW.status, vessel_id [%], null status, set to default moored from [%]', NEW.vessel_id, NEW.status;
|
||||
NEW.status := 'moored';
|
||||
END IF;
|
||||
IF previous_status IS NULL THEN
|
||||
IF previous_metric.status IS NULL THEN
|
||||
IF NEW.status = 'anchored' THEN
|
||||
RAISE WARNING 'Metrics Unknown previous_status from vessel_id [%], [%] set to default current status [%]', NEW.vessel_id, previous_status, NEW.status;
|
||||
previous_status := NEW.status;
|
||||
RAISE WARNING 'Metrics Unknown previous_metric.status from vessel_id [%], [%] set to default current status [%]', NEW.vessel_id, previous_metric.status, NEW.status;
|
||||
previous_metric.status := NEW.status;
|
||||
ELSE
|
||||
RAISE WARNING 'Metrics Unknown previous_status from vessel_id [%], [%] set to default status moored vs [%]', NEW.vessel_id, previous_status, NEW.status;
|
||||
previous_status := 'moored';
|
||||
RAISE WARNING 'Metrics Unknown previous_metric.status from vessel_id [%], [%] set to default status moored vs [%]', NEW.vessel_id, previous_metric.status, NEW.status;
|
||||
previous_metric.status := 'moored';
|
||||
END IF;
|
||||
-- Add new stay as no previous entry exist
|
||||
INSERT INTO api.stays
|
||||
@@ -405,13 +455,13 @@ CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
|
||||
-- Add stay entry to process queue for further processing
|
||||
INSERT INTO process_queue (channel, payload, stored, ref_id)
|
||||
VALUES ('new_stay', stay_id, now(), current_setting('vessel.id', true));
|
||||
RAISE WARNING 'Metrics Insert first stay as no previous metrics exist, stay_id %', stay_id;
|
||||
RAISE WARNING 'Metrics Insert first stay as no previous metrics exist, stay_id stay_id [%] [%] [%]', stay_id, NEW.status, NEW.time;
|
||||
END IF;
|
||||
-- Check if status is valid enum
|
||||
SELECT NEW.status::name = any(enum_range(null::status)::name[]) INTO valid_status;
|
||||
SELECT NEW.status::name = any(enum_range(null::status_type)::name[]) INTO valid_status;
|
||||
IF valid_status IS False THEN
|
||||
-- Ignore entry if status is invalid
|
||||
RAISE WARNING 'Metrics Ignoring metric, invalid status [%]', NEW.status;
|
||||
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], invalid status [%]', NEW.vessel_id, NEW.status;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
-- Check if speedOverGround is valid value
|
||||
@@ -423,10 +473,10 @@ CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
|
||||
|
||||
-- Check the state and if any previous/current entry
|
||||
-- If change of state and new status is sailing or motoring
|
||||
IF previous_status::TEXT <> NEW.status::TEXT AND
|
||||
( (NEW.status::TEXT = 'sailing' AND previous_status::TEXT <> 'motoring')
|
||||
OR (NEW.status::TEXT = 'motoring' AND previous_status::TEXT <> 'sailing') ) THEN
|
||||
RAISE WARNING 'Metrics Update status, try new logbook, New:[%] Previous:[%]', NEW.status, previous_status;
|
||||
IF previous_metric.status::TEXT <> NEW.status::TEXT AND
|
||||
( (NEW.status::TEXT = 'sailing' AND previous_metric.status::TEXT <> 'motoring')
|
||||
OR (NEW.status::TEXT = 'motoring' AND previous_metric.status::TEXT <> 'sailing') ) THEN
|
||||
RAISE WARNING 'Metrics Update status, try new logbook, New:[%] Previous:[%]', NEW.status, previous_metric.status;
|
||||
-- Start new log
|
||||
logbook_id := public.trip_in_progress_fn(current_setting('vessel.id', true)::TEXT);
|
||||
IF logbook_id IS NULL THEN
|
||||
@@ -434,7 +484,7 @@ CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
|
||||
(vessel_id, active, _from_time, _from_lat, _from_lng)
|
||||
VALUES (current_setting('vessel.id', true), true, NEW.time, NEW.latitude, NEW.longitude)
|
||||
RETURNING id INTO logbook_id;
|
||||
RAISE WARNING 'Metrics Insert new logbook, logbook_id %', logbook_id;
|
||||
RAISE WARNING 'Metrics Insert new logbook, logbook_id [%] [%] [%]', logbook_id, NEW.status, NEW.time;
|
||||
ELSE
|
||||
UPDATE api.logbook
|
||||
SET
|
||||
@@ -443,7 +493,7 @@ CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
|
||||
_to_lat = NEW.latitude,
|
||||
_to_lng = NEW.longitude
|
||||
WHERE id = logbook_id;
|
||||
RAISE WARNING 'Metrics Existing Logbook logbook_id [%] [%] [%]', logbook_id, NEW.status, NEW.time;
|
||||
RAISE WARNING 'Metrics Existing logbook logbook_id [%] [%] [%]', logbook_id, NEW.status, NEW.time;
|
||||
END IF;
|
||||
|
||||
-- End current stay
|
||||
@@ -454,20 +504,20 @@ CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
|
||||
active = false,
|
||||
departed = NEW.time
|
||||
WHERE id = stay_id;
|
||||
RAISE WARNING 'Metrics Updating Stay end current stay_id [%] [%] [%]', stay_id, NEW.status, NEW.time;
|
||||
-- Add moorage entry to process queue for further processing
|
||||
-- Add stay entry to process queue for further processing
|
||||
INSERT INTO process_queue (channel, payload, stored, ref_id)
|
||||
VALUES ('new_moorage', stay_id, now(), current_setting('vessel.id', true));
|
||||
VALUES ('new_stay', stay_id, NOW(), current_setting('vessel.id', true));
|
||||
RAISE WARNING 'Metrics Updating Stay end current stay_id [%] [%] [%]', stay_id, NEW.status, NEW.time;
|
||||
ELSE
|
||||
RAISE WARNING 'Metrics Invalid stay_id [%] [%]', stay_id, NEW.time;
|
||||
END IF;
|
||||
|
||||
-- If change of state and new status is moored or anchored
|
||||
ELSIF previous_status::TEXT <> NEW.status::TEXT AND
|
||||
( (NEW.status::TEXT = 'moored' AND previous_status::TEXT <> 'anchored')
|
||||
OR (NEW.status::TEXT = 'anchored' AND previous_status::TEXT <> 'moored') ) THEN
|
||||
ELSIF previous_metric.status::TEXT <> NEW.status::TEXT AND
|
||||
( (NEW.status::TEXT = 'moored' AND previous_metric.status::TEXT <> 'anchored')
|
||||
OR (NEW.status::TEXT = 'anchored' AND previous_metric.status::TEXT <> 'moored') ) THEN
|
||||
-- Start new stays
|
||||
RAISE WARNING 'Metrics Update status, try new stay, New:[%] Previous:[%]', NEW.status, previous_status;
|
||||
RAISE WARNING 'Metrics Update status, try new stay, New:[%] Previous:[%]', NEW.status, previous_metric.status;
|
||||
stay_id := public.stay_in_progress_fn(current_setting('vessel.id', true)::TEXT);
|
||||
IF stay_id IS NULL THEN
|
||||
RAISE WARNING 'Metrics Inserting new stay [%]', NEW.status;
|
||||
@@ -481,15 +531,14 @@ CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
|
||||
(vessel_id, active, arrived, latitude, longitude, stay_code)
|
||||
VALUES (current_setting('vessel.id', true), true, NEW.time, NEW.latitude, NEW.longitude, stay_code)
|
||||
RETURNING id INTO stay_id;
|
||||
-- Add stay entry to process queue for further processing
|
||||
INSERT INTO process_queue (channel, payload, stored, ref_id)
|
||||
VALUES ('new_stay', stay_id, now(), current_setting('vessel.id', true));
|
||||
RAISE WARNING 'Metrics Insert new stay, stay_id [%] [%] [%]', stay_id, NEW.status, NEW.time;
|
||||
ELSE
|
||||
RAISE WARNING 'Metrics Invalid stay_id [%] [%]', stay_id, NEW.time;
|
||||
UPDATE api.stays
|
||||
SET
|
||||
active = false,
|
||||
departed = NEW.time
|
||||
departed = NEW.time,
|
||||
notes = 'Invalid stay?'
|
||||
WHERE id = stay_id;
|
||||
END IF;
|
||||
|
||||
@@ -508,9 +557,9 @@ CREATE FUNCTION metrics_trigger_fn() RETURNS trigger AS $metrics$
|
||||
WHERE id = logbook_id;
|
||||
-- Add logbook entry to process queue for later processing
|
||||
INSERT INTO process_queue (channel, payload, stored, ref_id)
|
||||
VALUEs ('new_logbook', logbook_id, now(), current_setting('vessel.id', true));
|
||||
VALUES ('pre_logbook', logbook_id, NOW(), current_setting('vessel.id', true));
|
||||
ELSE
|
||||
RAISE WARNING 'Metrics Invalid logbook_id [%] [%]', logbook_id, NEW.time;
|
||||
RAISE WARNING 'Metrics Invalid logbook_id [%] [%] [%]', logbook_id, NEW.status, NEW.time;
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW; -- Finally insert the actual new metric
|
||||
@@ -519,7 +568,7 @@ $metrics$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.metrics_trigger_fn
|
||||
IS 'process metrics from vessel, generate new_logbook and new_stay.';
|
||||
IS 'process metrics from vessel, generate pre_logbook and new_stay.';
|
||||
|
||||
--
|
||||
-- Triggers logbook update on metrics insert
|
||||
@@ -529,3 +578,117 @@ CREATE TRIGGER metrics_trigger BEFORE INSERT ON api.metrics
|
||||
COMMENT ON TRIGGER
|
||||
metrics_trigger ON api.metrics
|
||||
IS 'BEFORE INSERT ON api.metrics run function metrics_trigger_fn';
|
||||
|
||||
-- Function update of name and stay_code on logbook and stays reference
|
||||
DROP FUNCTION IF EXISTS moorage_update_trigger_fn;
|
||||
CREATE FUNCTION moorage_update_trigger_fn() RETURNS trigger AS $moorage_update$
|
||||
DECLARE
|
||||
BEGIN
|
||||
RAISE NOTICE 'moorages_update_trigger_fn [%]', NEW;
|
||||
IF ( OLD.name != NEW.name) THEN
|
||||
UPDATE api.logbook SET _from = NEW.name WHERE _from_moorage_id = NEW.id;
|
||||
UPDATE api.logbook SET _to = NEW.name WHERE _to_moorage_id = NEW.id;
|
||||
END IF;
|
||||
IF ( OLD.stay_code != NEW.stay_code) THEN
|
||||
UPDATE api.stays SET stay_code = NEW.stay_code WHERE moorage_id = NEW.id;
|
||||
END IF;
|
||||
RETURN NULL; -- result is ignored since this is an AFTER trigger
|
||||
END;
|
||||
$moorage_update$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.moorage_update_trigger_fn
|
||||
IS 'Automatic update of name and stay_code on logbook and stays reference';
|
||||
|
||||
-- Triggers moorage update after update
|
||||
CREATE TRIGGER moorage_update_trigger AFTER UPDATE ON api.moorages
|
||||
FOR EACH ROW EXECUTE FUNCTION moorage_update_trigger_fn();
|
||||
-- Description
|
||||
COMMENT ON TRIGGER moorage_update_trigger
|
||||
ON api.moorages
|
||||
IS 'Automatic update of name and stay_code on logbook and stays reference';
|
||||
|
||||
-- Function delete logbook and stays reference when delete a moorage
|
||||
DROP FUNCTION IF EXISTS moorage_delete_trigger_fn;
|
||||
CREATE FUNCTION moorage_delete_trigger_fn() RETURNS trigger AS $moorage_delete$
|
||||
DECLARE
|
||||
BEGIN
|
||||
RAISE NOTICE 'moorages_delete_trigger_fn [%]', OLD;
|
||||
DELETE FROM api.stays WHERE moorage_id = OLD.id;
|
||||
DELETE FROM api.logbook WHERE _from_moorage_id = OLD.id;
|
||||
DELETE FROM api.logbook WHERE _to_moorage_id = OLD.id;
|
||||
RETURN OLD; -- result is ignored since this is an AFTER trigger
|
||||
END;
|
||||
$moorage_delete$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.moorage_delete_trigger_fn
|
||||
IS 'Automatic delete logbook and stays reference when delete a moorage';
|
||||
|
||||
-- Triggers moorage delete
|
||||
CREATE TRIGGER moorage_delete_trigger BEFORE DELETE ON api.moorages
|
||||
FOR EACH ROW EXECUTE FUNCTION moorage_delete_trigger_fn();
|
||||
-- Description
|
||||
COMMENT ON TRIGGER moorage_delete_trigger
|
||||
ON api.moorages
|
||||
IS 'Automatic delete logbook and stays reference when delete a moorage';
|
||||
|
||||
-- Function process_new on completed logbook
|
||||
DROP FUNCTION IF EXISTS logbook_completed_trigger_fn;
|
||||
CREATE FUNCTION logbook_completed_trigger_fn() RETURNS trigger AS $logbook_completed$
|
||||
DECLARE
|
||||
BEGIN
|
||||
RAISE NOTICE 'logbook_completed_trigger_fn [%]', OLD;
|
||||
RAISE NOTICE 'logbook_completed_trigger_fn [%] [%]', OLD._to_time, NEW._to_time;
|
||||
-- Add logbook entry to process queue for later processing
|
||||
--IF ( OLD._to_time <> NEW._to_time ) THEN
|
||||
INSERT INTO process_queue (channel, payload, stored, ref_id)
|
||||
VALUES ('new_logbook', NEW.id, NOW(), current_setting('vessel.id', true));
|
||||
--END IF;
|
||||
RETURN OLD; -- result is ignored since this is an AFTER trigger
|
||||
END;
|
||||
$logbook_completed$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.logbook_completed_trigger_fn
|
||||
IS 'Automatic process_queue for completed logbook._to_time';
|
||||
|
||||
-- Triggers logbook completed
|
||||
--CREATE TRIGGER logbook_completed_trigger AFTER UPDATE ON api.logbook
|
||||
-- FOR EACH ROW
|
||||
-- WHEN (OLD._to_time IS DISTINCT FROM NEW._to_time)
|
||||
-- EXECUTE FUNCTION logbook_completed_trigger_fn();
|
||||
-- Description
|
||||
--COMMENT ON TRIGGER logbook_completed_trigger
|
||||
-- ON api.logbook
|
||||
-- IS 'Automatic process_queue for completed logbook';
|
||||
|
||||
-- Function process_new on completed Stay
|
||||
DROP FUNCTION IF EXISTS stay_completed_trigger_fn;
|
||||
CREATE FUNCTION stay_completed_trigger_fn() RETURNS trigger AS $stay_completed$
|
||||
DECLARE
|
||||
BEGIN
|
||||
RAISE NOTICE 'stay_completed_trigger_fn [%]', OLD;
|
||||
RAISE NOTICE 'stay_completed_trigger_fn [%] [%]', OLD.departed, NEW.departed;
|
||||
-- Add stay entry to process queue for later processing
|
||||
--IF ( OLD.departed <> NEW.departed ) THEN
|
||||
INSERT INTO process_queue (channel, payload, stored, ref_id)
|
||||
VALUES ('new_stay', NEW.id, NOW(), current_setting('vessel.id', true));
|
||||
--END IF;
|
||||
RETURN OLD; -- result is ignored since this is an AFTER trigger
|
||||
END;
|
||||
$stay_completed$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.stay_completed_trigger_fn
|
||||
IS 'Automatic process_queue for completed stay.departed';
|
||||
|
||||
-- Triggers stay completed
|
||||
--CREATE TRIGGER stay_completed_trigger AFTER UPDATE ON api.stays
|
||||
-- FOR EACH ROW
|
||||
-- WHEN (OLD.departed IS DISTINCT FROM NEW.departed)
|
||||
-- EXECUTE FUNCTION stay_completed_trigger_fn();
|
||||
-- Description
|
||||
--COMMENT ON TRIGGER stay_completed_trigger
|
||||
-- ON api.stays
|
||||
-- IS 'Automatic process_queue for completed stay';
|
||||
|
@@ -7,6 +7,12 @@
|
||||
--
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
-- PostgREST Media Type Handlers
|
||||
CREATE DOMAIN "text/xml" AS xml;
|
||||
CREATE DOMAIN "application/geo+json" AS jsonb;
|
||||
CREATE DOMAIN "application/gpx+xml" AS xml;
|
||||
CREATE DOMAIN "application/vnd.google-earth.kml+xml" AS xml;
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Functions API schema
|
||||
-- Timelapse - replay logs
|
||||
@@ -16,47 +22,141 @@ CREATE OR REPLACE FUNCTION api.timelapse_fn(
|
||||
IN end_log INTEGER DEFAULT NULL,
|
||||
IN start_date TEXT DEFAULT NULL,
|
||||
IN end_date TEXT DEFAULT NULL,
|
||||
OUT geojson JSON) RETURNS JSON AS $timelapse$
|
||||
OUT geojson JSONB) RETURNS JSONB AS $timelapse$
|
||||
DECLARE
|
||||
_geojson jsonb;
|
||||
BEGIN
|
||||
-- TODO using jsonb pgsql function instead of python
|
||||
-- Using sub query to force id order by
|
||||
-- Merge GIS track_geom into a GeoJSON MultiLineString
|
||||
IF start_log IS NOT NULL AND public.isnumeric(start_log::text) AND public.isnumeric(end_log::text) THEN
|
||||
SELECT jsonb_agg(track_geojson->'features') INTO _geojson
|
||||
FROM api.logbook
|
||||
WHERE id >= start_log
|
||||
AND id <= end_log
|
||||
AND track_geojson IS NOT NULL;
|
||||
--raise WARNING 'by log _geojson %' , _geojson;
|
||||
WITH logbook as (
|
||||
SELECT track_geom
|
||||
FROM api.logbook
|
||||
WHERE id >= start_log
|
||||
AND id <= end_log
|
||||
AND track_geom IS NOT NULL
|
||||
ORDER BY _from_time ASC
|
||||
)
|
||||
SELECT ST_AsGeoJSON(geo.*) INTO _geojson FROM (
|
||||
SELECT ST_Collect(
|
||||
ARRAY(
|
||||
SELECT track_geom FROM logbook))
|
||||
) as geo;
|
||||
--raise WARNING 'by log id _geojson %' , _geojson;
|
||||
ELSIF start_date IS NOT NULL AND public.isdate(start_date::text) AND public.isdate(end_date::text) THEN
|
||||
SELECT jsonb_agg(track_geojson->'features') INTO _geojson
|
||||
FROM api.logbook
|
||||
WHERE _from_time >= start_log::TIMESTAMP WITHOUT TIME ZONE
|
||||
AND _to_time <= end_date::TIMESTAMP WITHOUT TIME ZONE + interval '23 hours 59 minutes'
|
||||
AND track_geojson IS NOT NULL;
|
||||
WITH logbook as (
|
||||
SELECT track_geom
|
||||
FROM api.logbook
|
||||
WHERE _from_time >= start_date::TIMESTAMPTZ
|
||||
AND _to_time <= end_date::TIMESTAMPTZ + interval '23 hours 59 minutes'
|
||||
AND track_geom IS NOT NULL
|
||||
ORDER BY _from_time ASC
|
||||
)
|
||||
SELECT ST_AsGeoJSON(geo.*) INTO _geojson FROM (
|
||||
SELECT ST_Collect(
|
||||
ARRAY(
|
||||
SELECT track_geom FROM logbook))
|
||||
) as geo;
|
||||
--raise WARNING 'by date _geojson %' , _geojson;
|
||||
ELSE
|
||||
SELECT jsonb_agg(track_geojson->'features') INTO _geojson
|
||||
FROM api.logbook
|
||||
WHERE track_geojson IS NOT NULL;
|
||||
WITH logbook as (
|
||||
SELECT track_geom
|
||||
FROM api.logbook
|
||||
WHERE track_geom IS NOT NULL
|
||||
ORDER BY _from_time ASC
|
||||
)
|
||||
SELECT ST_AsGeoJSON(geo.*) INTO _geojson FROM (
|
||||
SELECT ST_Collect(
|
||||
ARRAY(
|
||||
SELECT track_geom FROM logbook))
|
||||
) as geo;
|
||||
--raise WARNING 'all result _geojson %' , _geojson;
|
||||
END IF;
|
||||
-- Return a GeoJSON filter on Point
|
||||
-- Return a GeoJSON MultiLineString
|
||||
-- result _geojson [null, null]
|
||||
--raise WARNING 'result _geojson %' , _geojson;
|
||||
SELECT json_build_object(
|
||||
SELECT jsonb_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', public.geojson_py_fn(_geojson, 'LineString'::TEXT) ) INTO geojson;
|
||||
'features', ARRAY[_geojson] ) INTO geojson;
|
||||
END;
|
||||
$timelapse$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.timelapse_fn
|
||||
IS 'Export to geojson feature point with Time and courseOverGroundTrue properties';
|
||||
IS 'Export all selected logs geometry `track_geom` to a geojson as MultiLineString with empty properties';
|
||||
|
||||
DROP FUNCTION IF EXISTS api.timelapse2_fn;
|
||||
CREATE OR REPLACE FUNCTION api.timelapse2_fn(
|
||||
IN start_log INTEGER DEFAULT NULL,
|
||||
IN end_log INTEGER DEFAULT NULL,
|
||||
IN start_date TEXT DEFAULT NULL,
|
||||
IN end_date TEXT DEFAULT NULL,
|
||||
OUT geojson JSONB) RETURNS JSONB AS $timelapse2$
|
||||
DECLARE
|
||||
_geojson jsonb;
|
||||
BEGIN
|
||||
-- Using sub query to force id order by
|
||||
-- Merge GIS track_geom into a GeoJSON Points
|
||||
IF start_log IS NOT NULL AND public.isnumeric(start_log::text) AND public.isnumeric(end_log::text) THEN
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object('type', 'Feature',
|
||||
'properties', jsonb_build_object( 'notes', f->'properties'->>'notes'),
|
||||
'geometry', jsonb_build_object( 'coordinates', f->'geometry'->'coordinates', 'type', 'Point'))
|
||||
) INTO _geojson
|
||||
FROM (
|
||||
SELECT jsonb_array_elements(track_geojson->'features') AS f
|
||||
FROM api.logbook
|
||||
WHERE id >= start_log
|
||||
AND id <= end_log
|
||||
AND track_geojson IS NOT NULL
|
||||
ORDER BY _from_time ASC
|
||||
) AS sub
|
||||
WHERE (f->'geometry'->>'type') = 'Point';
|
||||
ELSIF start_date IS NOT NULL AND public.isdate(start_date::text) AND public.isdate(end_date::text) THEN
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object('type', 'Feature',
|
||||
'properties', jsonb_build_object( 'notes', f->'properties'->>'notes'),
|
||||
'geometry', jsonb_build_object( 'coordinates', f->'geometry'->'coordinates', 'type', 'Point'))
|
||||
) INTO _geojson
|
||||
FROM (
|
||||
SELECT jsonb_array_elements(track_geojson->'features') AS f
|
||||
FROM api.logbook
|
||||
WHERE _from_time >= start_date::TIMESTAMPTZ
|
||||
AND _to_time <= end_date::TIMESTAMPTZ + interval '23 hours 59 minutes'
|
||||
AND track_geojson IS NOT NULL
|
||||
ORDER BY _from_time ASC
|
||||
) AS sub
|
||||
WHERE (f->'geometry'->>'type') = 'Point';
|
||||
ELSE
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object('type', 'Feature',
|
||||
'properties', jsonb_build_object( 'notes', f->'properties'->>'notes'),
|
||||
'geometry', jsonb_build_object( 'coordinates', f->'geometry'->'coordinates', 'type', 'Point'))
|
||||
) INTO _geojson
|
||||
FROM (
|
||||
SELECT jsonb_array_elements(track_geojson->'features') AS f
|
||||
FROM api.logbook
|
||||
WHERE track_geojson IS NOT NULL
|
||||
ORDER BY _from_time ASC
|
||||
) AS sub
|
||||
WHERE (f->'geometry'->>'type') = 'Point';
|
||||
END IF;
|
||||
-- Return a GeoJSON MultiLineString
|
||||
-- result _geojson [null, null]
|
||||
raise WARNING 'result _geojson %' , _geojson;
|
||||
SELECT jsonb_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', _geojson ) INTO geojson;
|
||||
END;
|
||||
$timelapse2$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.timelapse2_fn
|
||||
IS 'Export all selected logs geometry `track_geom` to a geojson as points with notes properties';
|
||||
|
||||
-- export_logbook_geojson_fn
|
||||
DROP FUNCTION IF EXISTS api.export_logbook_geojson_fn;
|
||||
CREATE FUNCTION api.export_logbook_geojson_fn(IN _id integer, OUT geojson JSON) RETURNS JSON AS $export_logbook_geojson$
|
||||
CREATE FUNCTION api.export_logbook_geojson_fn(IN _id integer, OUT geojson JSONB) RETURNS JSONB AS $export_logbook_geojson$
|
||||
-- validate with geojson.io
|
||||
DECLARE
|
||||
logbook_rec record;
|
||||
@@ -80,41 +180,236 @@ $export_logbook_geojson$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.export_logbook_geojson_fn
|
||||
IS 'Export a log entry to geojson feature linestring and multipoint';
|
||||
IS 'Export a log entry to geojson with features LineString and Point';
|
||||
|
||||
-- Generate GPX XML file output
|
||||
-- https://opencpn.org/OpenCPN/info/gpxvalidation.html
|
||||
--
|
||||
DROP FUNCTION IF EXISTS api.export_logbook_gpx_fn;
|
||||
CREATE OR REPLACE FUNCTION api.export_logbook_gpx_fn(IN _id INTEGER, OUT gpx XML) RETURNS pg_catalog.xml
|
||||
CREATE OR REPLACE FUNCTION api.export_logbook_gpx_fn(IN _id INTEGER) RETURNS "text/xml"
|
||||
AS $export_logbook_gpx$
|
||||
DECLARE
|
||||
logbook_rec record;
|
||||
app_settings jsonb;
|
||||
BEGIN
|
||||
-- If _id is is not NULL and > 0
|
||||
IF _id IS NULL OR _id < 1 THEN
|
||||
RAISE WARNING '-> export_logbook_gpx_fn invalid input %', _id;
|
||||
RETURN;
|
||||
RETURN '';
|
||||
END IF;
|
||||
-- Gather log details
|
||||
SELECT * INTO logbook_rec
|
||||
FROM api.logbook WHERE id = _id;
|
||||
-- Ensure the query is successful
|
||||
IF logbook_rec.vessel_id IS NULL THEN
|
||||
RAISE WARNING '-> export_logbook_gpx_fn invalid logbook %', _id;
|
||||
RETURN;
|
||||
END IF;
|
||||
gpx := logbook_rec.track_gpx;
|
||||
END;
|
||||
-- Gather url from app settings
|
||||
app_settings := get_app_url_fn();
|
||||
--RAISE DEBUG '-> logbook_update_gpx_fn app_settings %', app_settings;
|
||||
-- Generate GPX XML, extract Point features from geojson.
|
||||
RETURN xmlelement(name gpx,
|
||||
xmlattributes( '1.1' as version,
|
||||
'PostgSAIL' as creator,
|
||||
'http://www.topografix.com/GPX/1/1' as xmlns,
|
||||
'http://www.opencpn.org' as "xmlns:opencpn",
|
||||
app_settings->>'app.url' as "xmlns:postgsail",
|
||||
'http://www.w3.org/2001/XMLSchema-instance' as "xmlns:xsi",
|
||||
'http://www.garmin.com/xmlschemas/GpxExtensions/v3' as "xmlns:gpxx",
|
||||
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www8.garmin.com/xmlschemas/GpxExtensionsv3.xsd' as "xsi:schemaLocation"),
|
||||
xmlelement(name metadata,
|
||||
xmlelement(name link, xmlattributes(app_settings->>'app.url' as href),
|
||||
xmlelement(name text, 'PostgSail'))),
|
||||
xmlelement(name trk,
|
||||
xmlelement(name name, l.name),
|
||||
xmlelement(name desc, l.notes),
|
||||
xmlelement(name link, xmlattributes(concat(app_settings->>'app.url', '/log/', l.id) as href),
|
||||
xmlelement(name text, l.name)),
|
||||
xmlelement(name extensions, xmlelement(name "postgsail:log_id", l.id),
|
||||
xmlelement(name "postgsail:link", concat(app_settings->>'app.url', '/log/', l.id)),
|
||||
xmlelement(name "opencpn:guid", uuid_generate_v4()),
|
||||
xmlelement(name "opencpn:viz", '1'),
|
||||
xmlelement(name "opencpn:start", l._from_time),
|
||||
xmlelement(name "opencpn:end", l._to_time)
|
||||
),
|
||||
xmlelement(name trkseg, xmlagg(
|
||||
xmlelement(name trkpt,
|
||||
xmlattributes(features->'geometry'->'coordinates'->1 as lat, features->'geometry'->'coordinates'->0 as lon),
|
||||
xmlelement(name time, features->'properties'->>'time')
|
||||
)))))::pg_catalog.xml
|
||||
FROM api.logbook l, jsonb_array_elements(track_geojson->'features') AS features
|
||||
WHERE features->'geometry'->>'type' = 'Point'
|
||||
AND l.id = _id
|
||||
GROUP BY l.name,l.notes,l.id;
|
||||
END;
|
||||
$export_logbook_gpx$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.export_logbook_gpx_fn
|
||||
IS 'Export a log entry to GPX XML format';
|
||||
|
||||
-- Generate KML XML file output
|
||||
-- https://developers.google.com/kml/documentation/kml_tut
|
||||
-- TODO https://developers.google.com/kml/documentation/time#timespans
|
||||
DROP FUNCTION IF EXISTS api.export_logbook_kml_fn;
|
||||
CREATE OR REPLACE FUNCTION api.export_logbook_kml_fn(IN _id INTEGER) RETURNS "text/xml"
|
||||
AS $export_logbook_kml$
|
||||
DECLARE
|
||||
logbook_rec record;
|
||||
BEGIN
|
||||
-- If _id is is not NULL and > 0
|
||||
IF _id IS NULL OR _id < 1 THEN
|
||||
RAISE WARNING '-> export_logbook_kml_fn invalid input %', _id;
|
||||
return '';
|
||||
END IF;
|
||||
-- Gather log details
|
||||
SELECT * INTO logbook_rec
|
||||
FROM api.logbook WHERE id = _id;
|
||||
-- Ensure the query is successful
|
||||
IF logbook_rec.vessel_id IS NULL THEN
|
||||
RAISE WARNING '-> export_logbook_kml_fn invalid logbook %', _id;
|
||||
return '';
|
||||
END IF;
|
||||
-- Extract POINT from LINESTRING to generate KML XML
|
||||
RETURN xmlelement(name kml,
|
||||
xmlattributes( '1.0' as version,
|
||||
'PostgSAIL' as creator,
|
||||
'http://www.w3.org/2005/Atom' as "xmlns:atom",
|
||||
'http://www.opengis.net/kml/2.2' as "xmlns",
|
||||
'http://www.google.com/kml/ext/2.2' as "xmlns:gx",
|
||||
'http://www.opengis.net/kml/2.2' as "xmlns:kml"),
|
||||
xmlelement(name "Document",
|
||||
xmlelement(name name, logbook_rec.name),
|
||||
xmlelement(name "Placemark",
|
||||
xmlelement(name name, logbook_rec.notes),
|
||||
ST_AsKML(logbook_rec.track_geom)::pg_catalog.xml)
|
||||
))::pg_catalog.xml
|
||||
FROM api.logbook WHERE id = _id;
|
||||
END;
|
||||
$export_logbook_kml$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.export_logbook_kml_fn
|
||||
IS 'Export a log entry to KML XML format';
|
||||
|
||||
DROP FUNCTION IF EXISTS api.export_logbooks_gpx_fn;
|
||||
CREATE OR REPLACE FUNCTION api.export_logbooks_gpx_fn(
|
||||
IN start_log INTEGER DEFAULT NULL,
|
||||
IN end_log INTEGER DEFAULT NULL) RETURNS "application/gpx+xml"
|
||||
AS $export_logbooks_gpx$
|
||||
declare
|
||||
merged_jsonb jsonb;
|
||||
app_settings jsonb;
|
||||
BEGIN
|
||||
-- Merge GIS track_geom of geometry type Point into a jsonb array format
|
||||
IF start_log IS NOT NULL AND public.isnumeric(start_log::text) AND public.isnumeric(end_log::text) THEN
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object('coordinates', f->'geometry'->'coordinates', 'time', f->'properties'->>'time')
|
||||
) INTO merged_jsonb
|
||||
FROM (
|
||||
SELECT jsonb_array_elements(track_geojson->'features') AS f
|
||||
FROM api.logbook
|
||||
WHERE id >= start_log
|
||||
AND id <= end_log
|
||||
AND track_geojson IS NOT NULL
|
||||
ORDER BY _from_time ASC
|
||||
) AS sub
|
||||
WHERE (f->'geometry'->>'type') = 'Point';
|
||||
ELSE
|
||||
SELECT jsonb_agg(
|
||||
jsonb_build_object('coordinates', f->'geometry'->'coordinates', 'time', f->'properties'->>'time')
|
||||
) INTO merged_jsonb
|
||||
FROM (
|
||||
SELECT jsonb_array_elements(track_geojson->'features') AS f
|
||||
FROM api.logbook
|
||||
WHERE track_geojson IS NOT NULL
|
||||
ORDER BY _from_time ASC
|
||||
) AS sub
|
||||
WHERE (f->'geometry'->>'type') = 'Point';
|
||||
END IF;
|
||||
--RAISE WARNING '-> export_logbooks_gpx_fn _jsonb %' , _jsonb;
|
||||
-- Gather url from app settings
|
||||
app_settings := get_app_url_fn();
|
||||
--RAISE WARNING '-> export_logbooks_gpx_fn app_settings %', app_settings;
|
||||
-- Generate GPX XML, extract Point features from geojson.
|
||||
RETURN xmlelement(name gpx,
|
||||
xmlattributes( '1.1' as version,
|
||||
'PostgSAIL' as creator,
|
||||
'http://www.topografix.com/GPX/1/1' as xmlns,
|
||||
'http://www.opencpn.org' as "xmlns:opencpn",
|
||||
app_settings->>'app.url' as "xmlns:postgsail"),
|
||||
xmlelement(name metadata,
|
||||
xmlelement(name link, xmlattributes(app_settings->>'app.url' as href),
|
||||
xmlelement(name text, 'PostgSail'))),
|
||||
xmlelement(name trk,
|
||||
xmlelement(name name, 'logbook name'),
|
||||
xmlelement(name trkseg, xmlagg(
|
||||
xmlelement(name trkpt,
|
||||
xmlattributes(features->'coordinates'->1 as lat, features->'coordinates'->0 as lon),
|
||||
xmlelement(name time, features->'properties'->>'time')
|
||||
)))))::pg_catalog.xml
|
||||
FROM jsonb_array_elements(merged_jsonb) AS features;
|
||||
END;
|
||||
$export_logbooks_gpx$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.export_logbooks_gpx_fn
|
||||
IS 'Export a logs entries to GPX XML format';
|
||||
|
||||
DROP FUNCTION IF EXISTS api.export_logbooks_kml_fn;
|
||||
CREATE OR REPLACE FUNCTION api.export_logbooks_kml_fn(
|
||||
IN start_log INTEGER DEFAULT NULL,
|
||||
IN end_log INTEGER DEFAULT NULL) RETURNS "text/xml"
|
||||
AS $export_logbooks_kml$
|
||||
DECLARE
|
||||
_geom geometry;
|
||||
app_settings jsonb;
|
||||
BEGIN
|
||||
-- Merge GIS track_geom into a GeoJSON MultiLineString
|
||||
IF start_log IS NOT NULL AND public.isnumeric(start_log::text) AND public.isnumeric(end_log::text) THEN
|
||||
WITH logbook as (
|
||||
SELECT track_geom
|
||||
FROM api.logbook
|
||||
WHERE id >= start_log
|
||||
AND id <= end_log
|
||||
AND track_geom IS NOT NULL
|
||||
ORDER BY _from_time ASC
|
||||
)
|
||||
SELECT ST_Collect(
|
||||
ARRAY(
|
||||
SELECT track_geom FROM logbook))
|
||||
into _geom;
|
||||
ELSE
|
||||
WITH logbook as (
|
||||
SELECT track_geom
|
||||
FROM api.logbook
|
||||
WHERE track_geom IS NOT NULL
|
||||
ORDER BY _from_time ASC
|
||||
)
|
||||
SELECT ST_Collect(
|
||||
ARRAY(
|
||||
SELECT track_geom FROM logbook))
|
||||
into _geom;
|
||||
--raise WARNING 'all result _geojson %' , _geojson;
|
||||
END IF;
|
||||
|
||||
-- Extract POINT from LINESTRING to generate KML XML
|
||||
RETURN xmlelement(name kml,
|
||||
xmlattributes( '1.0' as version,
|
||||
'PostgSAIL' as creator,
|
||||
'http://www.w3.org/2005/Atom' as "xmlns:atom",
|
||||
'http://www.opengis.net/kml/2.2' as "xmlns",
|
||||
'http://www.google.com/kml/ext/2.2' as "xmlns:gx",
|
||||
'http://www.opengis.net/kml/2.2' as "xmlns:kml"),
|
||||
xmlelement(name "Document",
|
||||
xmlelement(name name, 'logbook name'),
|
||||
xmlelement(name "Placemark",
|
||||
ST_AsKML(_geom)::pg_catalog.xml
|
||||
)
|
||||
)
|
||||
)::pg_catalog.xml;
|
||||
END;
|
||||
$export_logbooks_kml$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.export_logbooks_kml_fn
|
||||
IS 'Export a logs entries to KML XML format';
|
||||
|
||||
-- Find all log from and to moorage geopoint within 100m
|
||||
DROP FUNCTION IF EXISTS api.find_log_from_moorage_fn;
|
||||
CREATE OR REPLACE FUNCTION api.find_log_from_moorage_fn(IN _id INTEGER, OUT geojson JSON) RETURNS JSON AS $find_log_from_moorage$
|
||||
CREATE OR REPLACE FUNCTION api.find_log_from_moorage_fn(IN _id INTEGER, OUT geojson JSONB) RETURNS JSONB AS $find_log_from_moorage$
|
||||
DECLARE
|
||||
moorage_rec record;
|
||||
_geojson jsonb;
|
||||
@@ -137,7 +432,7 @@ CREATE OR REPLACE FUNCTION api.find_log_from_moorage_fn(IN _id INTEGER, OUT geoj
|
||||
1000 -- in meters ?
|
||||
);
|
||||
-- Return a GeoJSON filter on LineString
|
||||
SELECT json_build_object(
|
||||
SELECT jsonb_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', public.geojson_py_fn(_geojson, 'Point'::TEXT) ) INTO geojson;
|
||||
END;
|
||||
@@ -148,7 +443,7 @@ COMMENT ON FUNCTION
|
||||
IS 'Find all log from moorage geopoint within 100m';
|
||||
|
||||
DROP FUNCTION IF EXISTS api.find_log_to_moorage_fn;
|
||||
CREATE OR REPLACE FUNCTION api.find_log_to_moorage_fn(IN _id INTEGER, OUT geojson JSON) RETURNS JSON AS $find_log_to_moorage$
|
||||
CREATE OR REPLACE FUNCTION api.find_log_to_moorage_fn(IN _id INTEGER, OUT geojson JSONB) RETURNS JSONB AS $find_log_to_moorage$
|
||||
DECLARE
|
||||
moorage_rec record;
|
||||
_geojson jsonb;
|
||||
@@ -171,7 +466,7 @@ CREATE OR REPLACE FUNCTION api.find_log_to_moorage_fn(IN _id INTEGER, OUT geojso
|
||||
1000 -- in meters ?
|
||||
);
|
||||
-- Return a GeoJSON filter on LineString
|
||||
SELECT json_build_object(
|
||||
SELECT jsonb_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', public.geojson_py_fn(_geojson, 'Point'::TEXT) ) INTO geojson;
|
||||
END;
|
||||
@@ -309,7 +604,7 @@ DROP FUNCTION IF EXISTS api.export_moorages_geojson_fn;
|
||||
CREATE FUNCTION api.export_moorages_geojson_fn(OUT geojson JSONB) RETURNS JSONB AS $export_moorages_geojson$
|
||||
DECLARE
|
||||
BEGIN
|
||||
SELECT json_build_object(
|
||||
SELECT jsonb_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features',
|
||||
( SELECT
|
||||
@@ -332,16 +627,19 @@ COMMENT ON FUNCTION
|
||||
IS 'Export moorages as geojson';
|
||||
|
||||
DROP FUNCTION IF EXISTS api.export_moorages_gpx_fn;
|
||||
CREATE FUNCTION api.export_moorages_gpx_fn() RETURNS pg_catalog.xml AS $export_moorages_gpx$
|
||||
CREATE FUNCTION api.export_moorages_gpx_fn() RETURNS "text/xml" AS $export_moorages_gpx$
|
||||
DECLARE
|
||||
app_settings jsonb;
|
||||
BEGIN
|
||||
-- Gather url from app settings
|
||||
app_settings := get_app_url_fn();
|
||||
-- Generate XML
|
||||
RETURN xmlelement(name gpx,
|
||||
xmlattributes( '1.1' as version,
|
||||
'PostgSAIL' as creator,
|
||||
'http://www.topografix.com/GPX/1/1' as xmlns,
|
||||
'http://www.opencpn.org' as "xmlns:opencpn",
|
||||
'https://iot.openplotter.cloud' as "xmlns:postgsail",
|
||||
app_settings->>'app.url' as "xmlns:postgsail",
|
||||
'http://www.w3.org/2001/XMLSchema-instance' as "xmlns:xsi",
|
||||
'http://www.garmin.com/xmlschemas/GpxExtensions/v3' as "xmlns:gpxx",
|
||||
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www8.garmin.com/xmlschemas/GpxExtensionsv3.xsd' as "xsi:schemaLocation"),
|
||||
@@ -353,14 +651,14 @@ CREATE FUNCTION api.export_moorages_gpx_fn() RETURNS pg_catalog.xml AS $export_m
|
||||
concat('Last Stayed On: ', 'TODO last seen',
|
||||
E'\nTotal Stays: ', m.stay_duration,
|
||||
E'\nTotal Arrivals and Departures: ', m.reference_count,
|
||||
E'\nLink: ', concat('https://iot.openplotter.cloud/moorage/', m.id)),
|
||||
E'\nLink: ', concat(app_settings->>'app.url','/moorage/', m.id)),
|
||||
xmlelement(name "opencpn:guid", uuid_generate_v4())),
|
||||
xmlelement(name sym, 'anchor'),
|
||||
xmlelement(name type, 'WPT'),
|
||||
xmlelement(name link, xmlattributes(concat('https://iot.openplotter.cloud/moorage/', m.id) as href),
|
||||
xmlelement(name link, xmlattributes(concat(app_settings->>'app.url','moorage/', m.id) as href),
|
||||
xmlelement(name text, m.name)),
|
||||
xmlelement(name extensions, xmlelement(name "postgsail:mooorage_id", 1),
|
||||
xmlelement(name "postgsail:link", concat('https://iot.openplotter.cloud/moorage/', m.id)),
|
||||
xmlelement(name extensions, xmlelement(name "postgsail:mooorage_id", m.id),
|
||||
xmlelement(name "postgsail:link", concat(app_settings->>'app.url','/moorage/', m.id)),
|
||||
xmlelement(name "opencpn:guid", uuid_generate_v4()),
|
||||
xmlelement(name "opencpn:viz", '1'),
|
||||
xmlelement(name "opencpn:scale_min_max", xmlattributes(true as UseScale, 30000 as ScaleMin, 0 as ScaleMax)
|
||||
@@ -381,15 +679,15 @@ DROP FUNCTION IF EXISTS api.stats_logs_fn;
|
||||
CREATE OR REPLACE FUNCTION api.stats_logs_fn(
|
||||
IN start_date TEXT DEFAULT NULL,
|
||||
IN end_date TEXT DEFAULT NULL,
|
||||
OUT stats JSON) RETURNS JSON AS $stats_logs$
|
||||
OUT stats JSONB) RETURNS JSONB AS $stats_logs$
|
||||
DECLARE
|
||||
_start_date TIMESTAMP WITHOUT TIME ZONE DEFAULT '1970-01-01';
|
||||
_end_date TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW();
|
||||
_start_date TIMESTAMPTZ DEFAULT '1970-01-01';
|
||||
_end_date TIMESTAMPTZ DEFAULT NOW();
|
||||
BEGIN
|
||||
IF start_date IS NOT NULL AND public.isdate(start_date::text) AND public.isdate(end_date::text) THEN
|
||||
RAISE WARNING '--> stats_fn, filter result stats by date [%]', start_date;
|
||||
_start_date := start_date::TIMESTAMP WITHOUT TIME ZONE;
|
||||
_end_date := end_date::TIMESTAMP WITHOUT TIME ZONE;
|
||||
_start_date := start_date::TIMESTAMPTZ;
|
||||
_end_date := end_date::TIMESTAMPTZ;
|
||||
END IF;
|
||||
RAISE NOTICE '--> stats_fn, _start_date [%], _end_date [%]', _start_date, _end_date;
|
||||
WITH
|
||||
@@ -398,8 +696,8 @@ CREATE OR REPLACE FUNCTION api.stats_logs_fn(
|
||||
logs_view AS (
|
||||
SELECT *
|
||||
FROM api.logbook l
|
||||
WHERE _from_time >= _start_date::TIMESTAMP WITHOUT TIME ZONE
|
||||
AND _to_time <= _end_date::TIMESTAMP WITHOUT TIME ZONE + interval '23 hours 59 minutes'
|
||||
WHERE _from_time >= _start_date::TIMESTAMPTZ
|
||||
AND _to_time <= _end_date::TIMESTAMPTZ + interval '23 hours 59 minutes'
|
||||
),
|
||||
first_date AS (
|
||||
SELECT _from_time as first_date from logs_view ORDER BY first_date ASC LIMIT 1
|
||||
@@ -451,21 +749,21 @@ CREATE OR REPLACE FUNCTION api.stats_stays_fn(
|
||||
IN end_date TEXT DEFAULT NULL,
|
||||
OUT stats JSON) RETURNS JSON AS $stats_stays$
|
||||
DECLARE
|
||||
_start_date TIMESTAMP WITHOUT TIME ZONE DEFAULT '1970-01-01';
|
||||
_end_date TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW();
|
||||
_start_date TIMESTAMPTZ DEFAULT '1970-01-01';
|
||||
_end_date TIMESTAMPTZ DEFAULT NOW();
|
||||
BEGIN
|
||||
IF start_date IS NOT NULL AND public.isdate(start_date::text) AND public.isdate(end_date::text) THEN
|
||||
RAISE NOTICE '--> stats_stays_fn, custom filter result stats by date [%]', start_date;
|
||||
_start_date := start_date::TIMESTAMP WITHOUT TIME ZONE;
|
||||
_end_date := end_date::TIMESTAMP WITHOUT TIME ZONE;
|
||||
_start_date := start_date::TIMESTAMPTZ;
|
||||
_end_date := end_date::TIMESTAMPTZ;
|
||||
END IF;
|
||||
RAISE NOTICE '--> stats_stays_fn, _start_date [%], _end_date [%]', _start_date, _end_date;
|
||||
WITH
|
||||
moorages_log AS (
|
||||
SELECT s.id as stays_id, m.id as moorages_id, *
|
||||
FROM api.stays s, api.moorages m
|
||||
WHERE arrived >= _start_date::TIMESTAMP WITHOUT TIME ZONE
|
||||
AND departed <= _end_date::TIMESTAMP WITHOUT TIME ZONE + interval '23 hours 59 minutes'
|
||||
WHERE arrived >= _start_date::TIMESTAMPTZ
|
||||
AND departed <= _end_date::TIMESTAMPTZ + interval '23 hours 59 minutes'
|
||||
AND s.id = m.stay_id
|
||||
),
|
||||
home_ports AS (
|
||||
@@ -501,4 +799,140 @@ $stats_stays$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.stats_stays_fn
|
||||
IS 'Stays/Moorages stats by date';
|
||||
IS 'Stays/Moorages stats by date';
|
||||
|
||||
DROP FUNCTION IF EXISTS api.delete_logbook_fn;
|
||||
CREATE OR REPLACE FUNCTION api.delete_logbook_fn(IN _id integer) RETURNS BOOLEAN AS $delete_logbook$
|
||||
DECLARE
|
||||
logbook_rec record;
|
||||
previous_stays_id numeric;
|
||||
current_stays_departed text;
|
||||
current_stays_id numeric;
|
||||
current_stays_active boolean;
|
||||
BEGIN
|
||||
-- If _id is not NULL
|
||||
IF _id IS NULL OR _id < 1 THEN
|
||||
RAISE WARNING '-> delete_logbook_fn invalid input %', _id;
|
||||
RETURN FALSE;
|
||||
END IF;
|
||||
SELECT * INTO logbook_rec
|
||||
FROM api.logbook l
|
||||
WHERE id = _id;
|
||||
-- Update logbook
|
||||
UPDATE api.logbook l
|
||||
SET notes = 'mark for deletion'
|
||||
WHERE l.vessel_id = current_setting('vessel.id', false)
|
||||
AND id = logbook_rec.id;
|
||||
-- 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);
|
||||
-- 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;
|
||||
-- Update related stays
|
||||
UPDATE api.stays s
|
||||
SET notes = 'mark for deletion'
|
||||
WHERE s.vessel_id = current_setting('vessel.id', false)
|
||||
AND s.arrived = logbook_rec._to_time;
|
||||
-- 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
|
||||
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 '-> delete_logbook_fn delete logbook [%]', logbook_rec.id;
|
||||
DELETE FROM api.stays WHERE id = current_stays_id;
|
||||
RAISE WARNING '-> delete_logbook_fn delete stays [%]', current_stays_id;
|
||||
-- Clean up, Subtract (-1) moorages ref count
|
||||
UPDATE api.moorages
|
||||
SET reference_count = reference_count - 1
|
||||
WHERE vessel_id = current_setting('vessel.id', false)
|
||||
AND id = previous_stays_id;
|
||||
RETURN TRUE;
|
||||
END;
|
||||
$delete_logbook$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.delete_logbook_fn
|
||||
IS 'Delete a logbook and dependency stay';
|
||||
|
||||
CREATE OR REPLACE FUNCTION api.monitoring_history_fn(IN time_interval TEXT DEFAULT '24', OUT history_metrics JSONB) RETURNS JSONB AS $monitoring_history$
|
||||
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, time) AS time_bucket,
|
||||
avg((metrics->'environment.water.temperature')::numeric) AS waterTemperature,
|
||||
avg((metrics->'environment.inside.temperature')::numeric) AS insideTemperature,
|
||||
avg((metrics->'environment.outside.temperature')::numeric) AS outsideTemperature,
|
||||
avg((metrics->'environment.wind.speedOverGround')::numeric) AS windSpeedOverGround,
|
||||
avg((metrics->'environment.inside.relativeHumidity')::numeric) AS insideHumidity,
|
||||
avg((metrics->'environment.outside.relativeHumidity')::numeric) AS outsideHumidity,
|
||||
avg((metrics->'environment.outside.pressure')::numeric) AS outsidePressure,
|
||||
avg((metrics->'environment.inside.pressure')::numeric) AS insidePressure,
|
||||
avg((metrics->'electrical.batteries.House.capacity.stateOfCharge')::numeric) AS batteryCharge,
|
||||
avg((metrics->'electrical.batteries.House.voltage')::numeric) AS batteryVoltage,
|
||||
avg((metrics->'environment.depth.belowTransducer')::numeric) AS depth
|
||||
FROM api.metrics
|
||||
WHERE 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
|
||||
$monitoring_history$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.monitoring_history_fn
|
||||
IS 'Export metrics from a time period 24h, 48h, 72h, 7d';
|
||||
|
||||
CREATE OR REPLACE FUNCTION api.status_fn(out status jsonb) RETURNS JSONB AS $status_fn$
|
||||
DECLARE
|
||||
in_route BOOLEAN := False;
|
||||
BEGIN
|
||||
RAISE NOTICE '-> status_fn';
|
||||
SELECT EXISTS ( SELECT id
|
||||
FROM api.logbook l
|
||||
WHERE active IS True
|
||||
LIMIT 1
|
||||
) INTO in_route;
|
||||
IF in_route IS True THEN
|
||||
-- In route from <logbook.from_name> arrived at <>
|
||||
SELECT jsonb_build_object('status', sa.description, 'location', m.name, 'departed', l._from_time) INTO status
|
||||
from api.logbook l, api.stays_at sa, api.moorages m
|
||||
where s.stay_code = sa.stay_code AND l._from_moorage_id = m.id AND l.active IS True;
|
||||
ELSE
|
||||
-- At <Stat_at.Desc> in <Moorage.name> departed at <>
|
||||
SELECT jsonb_build_object('status', sa.description, 'location', m.name, 'arrived', s.arrived) INTO status
|
||||
from api.stays s, api.stays_at sa, api.moorages m
|
||||
where s.stay_code = sa.stay_code AND s.moorage_id = m.id AND s.active IS True;
|
||||
END IF;
|
||||
END
|
||||
$status_fn$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.status_fn
|
||||
IS 'generate vessel status';
|
@@ -11,24 +11,28 @@
|
||||
-- Views
|
||||
-- Views are invoked with the privileges of the view owner,
|
||||
-- make the user_role the view’s owner.
|
||||
-- to bypass this limit you need pg15+ with specific settings
|
||||
-- security_invoker=true,security_barrier=true
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
CREATE VIEW first_metric AS
|
||||
CREATE VIEW public.first_metric AS
|
||||
SELECT *
|
||||
FROM api.metrics
|
||||
ORDER BY time ASC LIMIT 1;
|
||||
|
||||
CREATE VIEW last_metric AS
|
||||
CREATE VIEW public.last_metric AS
|
||||
SELECT *
|
||||
FROM api.metrics
|
||||
ORDER BY time DESC LIMIT 1;
|
||||
|
||||
CREATE VIEW trip_in_progress AS
|
||||
DROP VIEW IF EXISTS public.trip_in_progress;
|
||||
CREATE VIEW public.trip_in_progress AS
|
||||
SELECT *
|
||||
FROM api.logbook
|
||||
WHERE active IS true;
|
||||
|
||||
CREATE VIEW stay_in_progress AS
|
||||
DROP VIEW IF EXISTS public.stay_in_progress;
|
||||
CREATE VIEW public.stay_in_progress AS
|
||||
SELECT *
|
||||
FROM api.stays
|
||||
WHERE active IS true;
|
||||
@@ -38,33 +42,37 @@ CREATE VIEW stay_in_progress AS
|
||||
DROP VIEW IF EXISTS api.logs_view;
|
||||
CREATE OR REPLACE VIEW api.logs_view WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT id,
|
||||
name as "Name",
|
||||
_from as "From",
|
||||
_from_time as "Started",
|
||||
_to as "To",
|
||||
_to_time as "Ended",
|
||||
distance as "Distance",
|
||||
duration as "Duration"
|
||||
name as "name",
|
||||
_from as "from",
|
||||
_from_time as "started",
|
||||
_to as "to",
|
||||
_to_time as "ended",
|
||||
distance as "distance",
|
||||
duration as "duration",
|
||||
_from_moorage_id,_to_moorage_id
|
||||
FROM api.logbook l
|
||||
WHERE _to_time IS NOT NULL
|
||||
WHERE name IS NOT NULL
|
||||
AND _to_time IS NOT NULL
|
||||
ORDER BY _from_time DESC;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
api.logs_view
|
||||
IS 'Logs web view';
|
||||
|
||||
-- Initial try of MATERIALIZED VIEW
|
||||
-- Initial try of MATERIALIZED VIEW - does not support RLS
|
||||
CREATE MATERIALIZED VIEW api.logs_mat_view AS
|
||||
SELECT id,
|
||||
name as "Name",
|
||||
_from as "From",
|
||||
_from_time as "Started",
|
||||
_to as "To",
|
||||
_to_time as "Ended",
|
||||
distance as "Distance",
|
||||
duration as "Duration"
|
||||
name as "name",
|
||||
_from as "from",
|
||||
_from_time as "started",
|
||||
_to as "to",
|
||||
_to_time as "ended",
|
||||
distance as "distance",
|
||||
duration as "duration",
|
||||
_from_moorage_id,_to_moorage_id
|
||||
FROM api.logbook l
|
||||
WHERE _to_time IS NOT NULL
|
||||
WHERE name IS NOT NULL
|
||||
AND _to_time IS NOT NULL
|
||||
ORDER BY _from_time DESC;
|
||||
-- Description
|
||||
COMMENT ON MATERIALIZED VIEW
|
||||
@@ -74,19 +82,21 @@ COMMENT ON MATERIALIZED VIEW
|
||||
DROP VIEW IF EXISTS api.log_view;
|
||||
CREATE OR REPLACE VIEW api.log_view WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT id,
|
||||
name as "Name",
|
||||
_from as "From",
|
||||
_from_time as "Started",
|
||||
_to as "To",
|
||||
_to_time as "Ended",
|
||||
distance as "Distance",
|
||||
duration as "Duration",
|
||||
notes as "Notes",
|
||||
name as "name",
|
||||
_from as "from",
|
||||
_from_time as "started",
|
||||
_to as "to",
|
||||
_to_time as "ended",
|
||||
distance as "distance",
|
||||
duration as "duration",
|
||||
notes as "notes",
|
||||
track_geojson as geojson,
|
||||
avg_speed as avg_speed,
|
||||
max_speed as max_speed,
|
||||
max_wind_speed as max_wind_speed,
|
||||
extra as extra
|
||||
extra as extra,
|
||||
_from_moorage_id as from_moorage_id,
|
||||
_to_moorage_id as to_moorage_id
|
||||
FROM api.logbook l
|
||||
WHERE _to_time IS NOT NULL
|
||||
ORDER BY _from_time DESC;
|
||||
@@ -96,34 +106,32 @@ COMMENT ON VIEW
|
||||
IS 'Log web view';
|
||||
|
||||
-- Stays web view
|
||||
-- TODO group by month
|
||||
DROP VIEW IF EXISTS api.stays_view;
|
||||
CREATE OR REPLACE VIEW api.stays_view WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT s.id,
|
||||
concat(
|
||||
extract(DAYS FROM (s.departed-s.arrived)::interval),
|
||||
' days',
|
||||
--DATE_TRUNC('day', s.departed-s.arrived),
|
||||
' stay at ',
|
||||
s.name,
|
||||
' in ',
|
||||
RTRIM(TO_CHAR(s.departed, 'Month')),
|
||||
' ',
|
||||
TO_CHAR(s.departed, 'YYYY')
|
||||
) as "name",
|
||||
s.name AS "moorage",
|
||||
s.name AS "name",
|
||||
m.name AS "moorage",
|
||||
m.id AS "moorage_id",
|
||||
(s.departed-s.arrived) AS "duration",
|
||||
sa.description AS "stayed_at",
|
||||
sa.stay_code AS "stayed_at_id",
|
||||
s.arrived AS "arrived",
|
||||
_from.id as "arrived_log_id",
|
||||
_from._to_moorage_id as "arrived_from_moorage_id",
|
||||
_from._to as "arrived_from_moorage_name",
|
||||
s.departed AS "departed",
|
||||
_to.id AS "departed_log_id",
|
||||
_to._from_moorage_id AS "departed_to_moorage_id",
|
||||
_to._from AS "departed_to_moorage_name",
|
||||
s.notes AS "notes"
|
||||
FROM api.stays s, api.stays_at sa, api.moorages m
|
||||
WHERE departed IS NOT NULL
|
||||
FROM api.stays_at sa, api.moorages m, api.stays s
|
||||
LEFT JOIN api.logbook AS _from ON _from._from_time = s.departed
|
||||
LEFT JOIN api.logbook AS _to ON _to._to_time = s.arrived
|
||||
WHERE s.departed IS NOT NULL
|
||||
AND _from._to_moorage_id IS NOT NULL
|
||||
AND s.name IS NOT NULL
|
||||
AND s.stay_code = sa.stay_code
|
||||
AND s.id = m.stay_id
|
||||
AND s.moorage_id = m.id
|
||||
ORDER BY s.arrived DESC;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
@@ -133,30 +141,29 @@ COMMENT ON VIEW
|
||||
DROP VIEW IF EXISTS api.stay_view;
|
||||
CREATE OR REPLACE VIEW api.stay_view WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT s.id,
|
||||
concat(
|
||||
extract(DAYS FROM (s.departed-s.arrived)::interval),
|
||||
' days',
|
||||
--DATE_TRUNC('day', s.departed-s.arrived),
|
||||
' stay at ',
|
||||
s.name,
|
||||
' in ',
|
||||
RTRIM(TO_CHAR(s.departed, 'Month')),
|
||||
' ',
|
||||
TO_CHAR(s.departed, 'YYYY')
|
||||
) as "name",
|
||||
s.name AS "moorage",
|
||||
s.name AS "name",
|
||||
m.name AS "moorage",
|
||||
m.id AS "moorage_id",
|
||||
(s.departed-s.arrived) AS "duration",
|
||||
sa.description AS "stayed_at",
|
||||
sa.stay_code AS "stayed_at_id",
|
||||
s.arrived AS "arrived",
|
||||
_from.id as "arrived_log_id",
|
||||
_from._to_moorage_id as "arrived_from_moorage_id",
|
||||
_from._to as "arrived_from_moorage_name",
|
||||
s.departed AS "departed",
|
||||
_to.id AS "departed_log_id",
|
||||
_to._from_moorage_id AS "departed_to_moorage_id",
|
||||
_to._from AS "departed_to_moorage_name",
|
||||
s.notes AS "notes"
|
||||
FROM api.stays s, api.stays_at sa, api.moorages m
|
||||
WHERE departed IS NOT NULL
|
||||
FROM api.stays_at sa, api.moorages m, api.stays s
|
||||
LEFT JOIN api.logbook AS _from ON _from._from_time = s.departed
|
||||
LEFT JOIN api.logbook AS _to ON _to._to_time = s.arrived
|
||||
WHERE s.departed IS NOT NULL
|
||||
AND _from._to_moorage_id IS NOT NULL
|
||||
AND s.name IS NOT NULL
|
||||
AND s.stay_code = sa.stay_code
|
||||
AND s.id = m.stay_id
|
||||
AND s.moorage_id = m.id
|
||||
ORDER BY s.arrived DESC;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
@@ -187,17 +194,20 @@ CREATE OR REPLACE VIEW api.moorages_view WITH (security_invoker=true,security_ba
|
||||
sa.description AS Default_Stay,
|
||||
sa.stay_code AS Default_Stay_Id,
|
||||
EXTRACT(DAY FROM justify_hours ( m.stay_duration )) AS Total_Stay, -- in days
|
||||
m.stay_duration AS Total_Duration,
|
||||
m.reference_count AS Arrivals_Departures
|
||||
-- m.geog
|
||||
-- m.stay_duration,
|
||||
-- justify_hours ( m.stay_duration )
|
||||
FROM api.moorages m, api.stays_at sa
|
||||
WHERE m.name IS NOT NULL
|
||||
AND geog IS NOT NULL
|
||||
-- m.stay_duration is only process on a stay
|
||||
WHERE m.stay_duration IS NOT NULL
|
||||
AND m.geog IS NOT NULL
|
||||
AND m.stay_code = sa.stay_code
|
||||
GROUP BY m.id,m.name,sa.description,m.stay_duration,m.reference_count,m.geog,sa.stay_code
|
||||
-- ORDER BY 4 DESC;
|
||||
ORDER BY m.reference_count DESC;
|
||||
-- ORDER BY m.reference_count DESC;
|
||||
ORDER BY m.stay_duration DESC;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
api.moorages_view
|
||||
@@ -211,11 +221,13 @@ CREATE OR REPLACE VIEW api.moorage_view WITH (security_invoker=true,security_bar
|
||||
sa.stay_code AS Default_Stay_Id,
|
||||
m.home_flag AS Home,
|
||||
EXTRACT(DAY FROM justify_hours ( m.stay_duration )) AS Total_Stay,
|
||||
m.stay_duration AS Total_Duration,
|
||||
m.reference_count AS Arrivals_Departures,
|
||||
m.notes
|
||||
-- m.geog
|
||||
FROM api.moorages m, api.stays_at sa
|
||||
WHERE m.name IS NOT NULL
|
||||
-- m.stay_duration is only process on a stay
|
||||
WHERE m.stay_duration IS NOT NULL
|
||||
AND geog IS NOT NULL
|
||||
AND m.stay_code = sa.stay_code;
|
||||
-- Description
|
||||
@@ -223,6 +235,27 @@ COMMENT ON VIEW
|
||||
api.moorage_view
|
||||
IS 'Moorage details web view';
|
||||
|
||||
DROP VIEW IF EXISTS api.moorages_stays_view;
|
||||
CREATE OR REPLACE VIEW api.moorages_stays_view WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT
|
||||
_to.id AS _to_id,
|
||||
_to._to_time,
|
||||
_from.id AS _from_id,
|
||||
_from._from_time,
|
||||
s.stay_code,s.duration,m.id
|
||||
FROM api.stays_at sa, api.moorages m, api.stays s
|
||||
LEFT JOIN api.logbook AS _from ON _from._from_time = s.departed
|
||||
LEFT JOIN api.logbook AS _to ON _to._to_time = s.arrived
|
||||
WHERE s.departed IS NOT NULL
|
||||
AND s.name IS NOT NULL
|
||||
AND s.stay_code = sa.stay_code
|
||||
AND s.moorage_id = m.id
|
||||
ORDER BY _to._to_time DESC;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
api.moorages_stays_view
|
||||
IS 'Moorages stay listing web view';
|
||||
|
||||
-- All moorage in 100 meters from the start of a logbook.
|
||||
-- ST_DistanceSphere Returns minimum distance in meters between two lon/lat points.
|
||||
--SELECT
|
||||
@@ -257,15 +290,15 @@ CREATE OR REPLACE VIEW api.stats_logs_view WITH (security_invoker=true,security_
|
||||
SELECT m.time FROM api.metrics m ORDER BY m.time ASC limit 1),
|
||||
logbook AS (
|
||||
SELECT
|
||||
count(*) AS "Number of Log Entries",
|
||||
max(l.max_speed) AS "Max Speed",
|
||||
max(l.max_wind_speed) AS "Max Wind Speed",
|
||||
sum(l.distance) AS "Total Distance",
|
||||
sum(l.duration) AS "Total Time Underway",
|
||||
concat( max(l.distance), ' NM, ', max(l.duration), ' hours') AS "Longest Nonstop Sail"
|
||||
count(*) AS "number_of_log_entries",
|
||||
max(l.max_speed) AS "max_speed",
|
||||
max(l.max_wind_speed) AS "max_wind_speed",
|
||||
sum(l.distance) AS "total_distance",
|
||||
sum(l.duration) AS "total_time_underway",
|
||||
concat( max(l.distance), ' NM, ', max(l.duration), ' hours') AS "longest_nonstop_sail"
|
||||
FROM api.logbook l)
|
||||
SELECT
|
||||
m.name as Name,
|
||||
m.name AS name,
|
||||
fm.time AS first,
|
||||
lm.time AS last,
|
||||
l.*
|
||||
@@ -301,10 +334,10 @@ CREATE OR REPLACE VIEW api.stats_moorages_view WITH (security_invoker=true,secur
|
||||
select sum(m.stay_duration) as time_spent_away from api.moorages m where home_flag is false
|
||||
)
|
||||
SELECT
|
||||
home_ports.home_ports as "Home Ports",
|
||||
unique_moorage.unique_moorage as "Unique Moorages",
|
||||
time_at_home_ports.time_at_home_ports "Time Spent at Home Port(s)",
|
||||
time_spent_away.time_spent_away as "Time Spent Away"
|
||||
home_ports.home_ports as "home_ports",
|
||||
unique_moorage.unique_moorage as "unique_moorages",
|
||||
time_at_home_ports.time_at_home_ports "time_spent_at_home_port(s)",
|
||||
time_spent_away.time_spent_away as "time_spent_away"
|
||||
FROM home_ports, unique_moorage, time_at_home_ports, time_spent_away;
|
||||
COMMENT ON VIEW
|
||||
api.stats_moorages_view
|
||||
@@ -345,23 +378,28 @@ CREATE VIEW api.monitoring_view WITH (security_invoker=true,security_barrier=tru
|
||||
metrics-> 'environment.inside.temperature' AS insideTemperature,
|
||||
metrics-> 'environment.outside.temperature' AS outsideTemperature,
|
||||
metrics-> 'environment.wind.speedOverGround' AS windSpeedOverGround,
|
||||
metrics-> 'environment.wind.directionGround' AS windDirectionGround,
|
||||
metrics-> 'environment.inside.humidity' AS insideHumidity,
|
||||
metrics-> 'environment.outside.humidity' AS outsideHumidity,
|
||||
metrics-> 'environment.wind.directionTrue' AS windDirectionTrue,
|
||||
metrics-> 'environment.inside.relativeHumidity' AS insideHumidity,
|
||||
metrics-> 'environment.outside.relativeHumidity' AS outsideHumidity,
|
||||
metrics-> 'environment.outside.pressure' AS outsidePressure,
|
||||
metrics-> 'environment.inside.pressure' AS insidePressure,
|
||||
metrics-> 'electrical.batteries.House.capacity.stateOfCharge' AS batteryCharge,
|
||||
metrics-> 'electrical.batteries.House.voltage' AS batteryVoltage,
|
||||
metrics-> 'environment.depth.belowTransducer' AS depth,
|
||||
jsonb_build_object(
|
||||
'type', 'Feature',
|
||||
'geometry', ST_AsGeoJSON(st_makepoint(longitude,latitude))::jsonb,
|
||||
'properties', jsonb_build_object(
|
||||
'name', current_setting('vessel.name', false),
|
||||
'latitude', m.latitude,
|
||||
'longitude', m.longitude
|
||||
'longitude', m.longitude,
|
||||
'time', m.time,
|
||||
'speedoverground', m.speedoverground,
|
||||
'windspeedapparent', m.windspeedapparent
|
||||
)::jsonb ) AS geojson,
|
||||
current_setting('vessel.name', false) AS name
|
||||
FROM api.metrics m
|
||||
--( SELECT api.status_fn() ) AS status
|
||||
FROM api.metrics m
|
||||
ORDER BY time DESC LIMIT 1;
|
||||
COMMENT ON VIEW
|
||||
api.monitoring_view
|
||||
@@ -372,7 +410,7 @@ CREATE VIEW api.monitoring_humidity WITH (security_invoker=true,security_barrier
|
||||
SELECT m.time, key, value
|
||||
FROM api.metrics m,
|
||||
jsonb_each_text(m.metrics)
|
||||
WHERE key ILIKE 'environment.%.humidity'
|
||||
WHERE key ILIKE 'environment.%.humidity' OR key ILIKE 'environment.%.relativeHumidity'
|
||||
ORDER BY m.time DESC;
|
||||
COMMENT ON VIEW
|
||||
api.monitoring_humidity
|
||||
@@ -454,3 +492,19 @@ CREATE VIEW api.total_info_view WITH (security_invoker=true,security_barrier=tru
|
||||
COMMENT ON VIEW
|
||||
api.total_info_view
|
||||
IS 'total_info_view web view';
|
||||
|
||||
DROP VIEW IF EXISTS api.explore_view;
|
||||
CREATE VIEW api.explore_view WITH (security_invoker=true,security_barrier=true) AS
|
||||
-- Expose last metrics
|
||||
WITH raw_metrics AS (
|
||||
SELECT m.time, m.metrics
|
||||
FROM api.metrics m
|
||||
ORDER BY m.time desc limit 1
|
||||
)
|
||||
SELECT raw_metrics.time, key, value
|
||||
FROM raw_metrics,
|
||||
jsonb_each_text(raw_metrics.metrics)
|
||||
ORDER BY key ASC;
|
||||
COMMENT ON VIEW
|
||||
api.explore_view
|
||||
IS 'explore_view web view';
|
||||
|
@@ -8,6 +8,36 @@ select current_database();
|
||||
-- connect to the DB
|
||||
\c signalk
|
||||
|
||||
-- Check for new logbook pending validation
|
||||
CREATE FUNCTION cron_process_pre_logbook_fn() RETURNS void AS $$
|
||||
DECLARE
|
||||
process_rec record;
|
||||
BEGIN
|
||||
-- Check for new logbook pending update
|
||||
RAISE NOTICE 'cron_process_pre_logbook_fn init loop';
|
||||
FOR process_rec in
|
||||
SELECT * FROM process_queue
|
||||
WHERE channel = 'pre_logbook' AND processed IS NULL
|
||||
ORDER BY stored ASC LIMIT 100
|
||||
LOOP
|
||||
RAISE NOTICE 'cron_process_pre_logbook_fn processing queue [%] for logbook id [%]', process_rec.id, process_rec.payload;
|
||||
-- update logbook
|
||||
PERFORM process_pre_logbook_fn(process_rec.payload::INTEGER);
|
||||
-- update process_queue table , processed
|
||||
UPDATE process_queue
|
||||
SET
|
||||
processed = NOW()
|
||||
WHERE id = process_rec.id;
|
||||
RAISE NOTICE 'cron_process_pre_logbook_fn processed queue [%] for logbook id [%]', process_rec.id, process_rec.payload;
|
||||
END LOOP;
|
||||
END;
|
||||
$$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.cron_process_pre_logbook_fn
|
||||
IS 'init by pg_cron to check for new logbook pending update, if so perform process_logbook_valid_fn';
|
||||
|
||||
|
||||
-- Check for new logbook pending update
|
||||
CREATE FUNCTION cron_process_new_logbook_fn() RETURNS void AS $$
|
||||
declare
|
||||
@@ -94,7 +124,7 @@ $$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.cron_process_new_moorage_fn
|
||||
IS 'init by pg_cron to check for new moorage pending update, if so perform process_moorage_queue_fn';
|
||||
IS 'Deprecated, init by pg_cron to check for new moorage pending update, if so perform process_moorage_queue_fn';
|
||||
|
||||
-- CRON Monitor offline pending notification
|
||||
create function cron_process_monitor_offline_fn() RETURNS void AS $$
|
||||
@@ -329,6 +359,53 @@ COMMENT ON FUNCTION
|
||||
public.cron_process_new_notification_fn
|
||||
IS 'init by pg_cron to check for new event pending notifications, if so perform process_notification_queue_fn';
|
||||
|
||||
-- CRON for new vessel metadata pending grafana provisioning
|
||||
CREATE FUNCTION cron_process_grafana_fn() RETURNS void AS $$
|
||||
DECLARE
|
||||
process_rec record;
|
||||
data_rec record;
|
||||
app_settings jsonb;
|
||||
user_settings jsonb;
|
||||
BEGIN
|
||||
-- We run grafana provisioning only after the first received vessel metadata
|
||||
-- Check for new vessel metadata pending grafana provisioning
|
||||
RAISE NOTICE 'cron_process_grafana_fn';
|
||||
FOR process_rec in
|
||||
SELECT * from process_queue
|
||||
where channel = 'grafana' and processed is null
|
||||
order by stored asc
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_grafana_fn [%]', process_rec.payload;
|
||||
-- Gather url from app settings
|
||||
app_settings := get_app_settings_fn();
|
||||
-- Get vessel details base on metadata id
|
||||
SELECT * INTO data_rec
|
||||
FROM api.metadata m, auth.vessels v
|
||||
WHERE m.id = process_rec.payload::INTEGER
|
||||
AND m.vessel_id = v.vessel_id;
|
||||
-- as we got data from the vessel we can do the grafana provisioning.
|
||||
PERFORM grafana_py_fn(data_rec.name, data_rec.vessel_id, data_rec.owner_email, app_settings);
|
||||
-- Gather user settings
|
||||
user_settings := get_user_settings_from_vesselid_fn(data_rec.vessel_id::TEXT);
|
||||
RAISE DEBUG '-> DEBUG cron_process_grafana_fn get_user_settings_from_vesselid_fn [%]', user_settings;
|
||||
-- add user in keycloak
|
||||
PERFORM keycloak_auth_py_fn(data_rec.vessel_id, user_settings, app_settings);
|
||||
-- Send notification
|
||||
PERFORM send_notification_fn('grafana'::TEXT, user_settings::JSONB);
|
||||
-- update process_queue entry as processed
|
||||
UPDATE process_queue
|
||||
SET
|
||||
processed = NOW()
|
||||
WHERE id = process_rec.id;
|
||||
RAISE NOTICE '-> cron_process_grafana_fn updated process_queue table [%]', process_rec.id;
|
||||
END LOOP;
|
||||
END;
|
||||
$$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.cron_process_grafana_fn
|
||||
IS 'init by pg_cron to check for new vessel pending grafana provisioning, if so perform grafana_py_fn';
|
||||
|
||||
-- CRON for Vacuum database
|
||||
CREATE FUNCTION cron_vacuum_fn() RETURNS void AS $$
|
||||
-- ERROR: VACUUM cannot be executed from a function
|
||||
@@ -348,25 +425,12 @@ COMMENT ON FUNCTION
|
||||
public.cron_vacuum_fn
|
||||
IS 'init by pg_cron to full vacuum tables on schema api';
|
||||
|
||||
-- CRON for clean up job details logs
|
||||
CREATE FUNCTION job_run_details_cleanup_fn() RETURNS void AS $$
|
||||
DECLARE
|
||||
BEGIN
|
||||
-- Remove job run log older than 3 months
|
||||
RAISE NOTICE 'job_run_details_cleanup_fn';
|
||||
DELETE FROM postgres.cron.job_run_details
|
||||
WHERE start_time <= NOW() AT TIME ZONE 'UTC' - INTERVAL '91 DAYS';
|
||||
END;
|
||||
$$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.job_run_details_cleanup_fn
|
||||
IS 'init by pg_cron to cleanup job_run_details table on schema public postgres db';
|
||||
|
||||
-- CRON for alerts notification
|
||||
CREATE FUNCTION cron_process_alerts_fn() RETURNS void AS $$
|
||||
DECLARE
|
||||
alert_rec record;
|
||||
last_metric TIMESTAMPTZ;
|
||||
metric_rec record;
|
||||
BEGIN
|
||||
-- Check for new event notification pending update
|
||||
RAISE NOTICE 'cron_process_alerts_fn';
|
||||
@@ -376,9 +440,24 @@ BEGIN
|
||||
FROM auth.accounts a, auth.vessels v, api.metadata m
|
||||
WHERE m.vessel_id = v.vessel_id
|
||||
AND a.email = v.owner_email
|
||||
AND (preferences->'alerting'->'enabled')::boolean = false
|
||||
AND (a.preferences->'alerting'->'enabled')::boolean = True
|
||||
AND m.active = True
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_alert_rec_fn for [%]', alert_rec;
|
||||
PERFORM set_config('vessel.id', alert_rec.vessel_id, false);
|
||||
--RAISE WARNING 'public.cron_process_alert_rec_fn() scheduler vessel.id %, user.id', current_setting('vessel.id', false), current_setting('user.id', false);
|
||||
-- Get time from the last metrics entry
|
||||
SELECT m.time INTO last_metric FROM api.metrics m WHERE vessel_id = alert_rec.vessel_id ORDER BY m.time DESC LIMIT 1;
|
||||
-- Get all metrics from the last 10 minutes
|
||||
FOR metric_rec in
|
||||
SELECT *
|
||||
FROM api.metrics m
|
||||
WHERE vessel_id = alert_rec.vessel_id
|
||||
AND time >= last_metric - INTERVAL '10 MINUTES'
|
||||
ORDER BY m.time DESC LIMIT 100
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_alert_rec_fn checking metrics [%]', metric_rec;
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
END;
|
||||
$$ language plpgsql;
|
||||
@@ -404,7 +483,7 @@ BEGIN
|
||||
WHERE v.owner_email = a.email)
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_no_vessel_rec_fn for [%]', no_vessel;
|
||||
SELECT json_build_object('email', no_vessel.email, 'recipient', a.first) into user_settings;
|
||||
SELECT json_build_object('email', no_vessel.email, 'recipient', no_vessel.first) into user_settings;
|
||||
RAISE NOTICE '-> debug cron_process_no_vessel_rec_fn [%]', user_settings;
|
||||
-- Send notification
|
||||
PERFORM send_notification_fn('no_vessel'::TEXT, user_settings::JSONB);
|
||||
@@ -433,8 +512,8 @@ BEGIN
|
||||
FROM api.metadata m
|
||||
WHERE v.vessel_id = m.vessel_id) AND v.owner_email = a.email
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_no_activity_rec_fn for [%]', no_metadata_rec;
|
||||
SELECT json_build_object('email', no_metadata_rec.email, 'recipient', a.first) into user_settings;
|
||||
RAISE NOTICE '-> cron_process_no_metadata_rec_fn for [%]', no_metadata_rec;
|
||||
SELECT json_build_object('email', no_metadata_rec.email, 'recipient', no_metadata_rec.first) into user_settings;
|
||||
RAISE NOTICE '-> debug cron_process_no_metadata_rec_fn [%]', user_settings;
|
||||
-- Send notification
|
||||
PERFORM send_notification_fn('no_metadata'::TEXT, user_settings::JSONB);
|
||||
@@ -452,17 +531,18 @@ DECLARE
|
||||
no_activity_rec record;
|
||||
user_settings jsonb;
|
||||
BEGIN
|
||||
-- Check for vessel with no activity for more than 200 days
|
||||
-- Check for vessel with no activity for more than 230 days
|
||||
RAISE NOTICE 'cron_process_no_activity_fn';
|
||||
FOR no_activity_rec in
|
||||
SELECT
|
||||
v.owner_email,m.name,m.vessel_id,m.time
|
||||
FROM api.metadata m
|
||||
LEFT JOIN auth.vessels v ON v.vessel_id = m.vessel_id
|
||||
WHERE m.time <= NOW() AT TIME ZONE 'UTC' - INTERVAL '200 DAYS'
|
||||
v.owner_email,m.name,m.vessel_id,m.time,a.first
|
||||
FROM auth.accounts a
|
||||
LEFT JOIN auth.vessels v ON v.owner_email = a.email
|
||||
LEFT JOIN api.metadata m ON v.vessel_id = m.vessel_id
|
||||
WHERE m.time < NOW() AT TIME ZONE 'UTC' - INTERVAL '230 DAYS'
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_no_activity_rec_fn for [%]', no_activity_rec;
|
||||
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', a.first) into user_settings;
|
||||
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
|
||||
RAISE NOTICE '-> debug cron_process_no_activity_rec_fn [%]', user_settings;
|
||||
-- Send notification
|
||||
PERFORM send_notification_fn('no_activity'::TEXT, user_settings::JSONB);
|
||||
@@ -472,4 +552,89 @@ $no_activity$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.cron_process_no_activity_fn
|
||||
IS 'init by pg_cron, check for vessel with no activity for more than 200 days then send notification';
|
||||
IS 'init by pg_cron, check for vessel with no activity for more than 230 days then send notification';
|
||||
|
||||
-- CRON for deactivated/deletion
|
||||
CREATE FUNCTION cron_process_deactivated_fn() RETURNS void AS $deactivated$
|
||||
DECLARE
|
||||
no_activity_rec record;
|
||||
user_settings jsonb;
|
||||
BEGIN
|
||||
RAISE NOTICE 'cron_process_deactivated_fn';
|
||||
|
||||
-- List accounts with vessel inactivity for more than 1 YEAR
|
||||
FOR no_activity_rec in
|
||||
SELECT
|
||||
v.owner_email,m.name,m.vessel_id,m.time,a.first
|
||||
FROM auth.accounts a
|
||||
LEFT JOIN auth.vessels v ON v.owner_email = a.email
|
||||
LEFT JOIN api.metadata m ON v.vessel_id = m.vessel_id
|
||||
WHERE m.time < NOW() AT TIME ZONE 'UTC' - INTERVAL '1 YEAR'
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_deactivated_rec_fn for inactivity [%]', no_activity_rec;
|
||||
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
|
||||
RAISE NOTICE '-> debug cron_process_deactivated_rec_fn inactivity [%]', user_settings;
|
||||
-- Send notification
|
||||
PERFORM send_notification_fn('deactivated'::TEXT, user_settings::JSONB);
|
||||
--PERFORM public.delete_account_fn(no_activity_rec.owner_email::TEXT, no_activity_rec.vessel_id::TEXT);
|
||||
END LOOP;
|
||||
|
||||
-- List accounts with no vessel metadata for more than 1 YEAR
|
||||
FOR no_activity_rec in
|
||||
SELECT
|
||||
a.user_id,a.email,a.first,a.created_at
|
||||
FROM auth.accounts a, auth.vessels v
|
||||
WHERE NOT EXISTS (
|
||||
SELECT *
|
||||
FROM api.metadata m
|
||||
WHERE v.vessel_id = m.vessel_id) AND v.owner_email = a.email
|
||||
AND v.created_at < NOW() AT TIME ZONE 'UTC' - INTERVAL '1 YEAR'
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_deactivated_rec_fn for no metadata [%]', no_activity_rec;
|
||||
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
|
||||
RAISE NOTICE '-> debug cron_process_deactivated_rec_fn no metadata [%]', user_settings;
|
||||
-- Send notification
|
||||
PERFORM send_notification_fn('deactivated'::TEXT, user_settings::JSONB);
|
||||
--PERFORM public.delete_account_fn(no_activity_rec.owner_email::TEXT, no_activity_rec.vessel_id::TEXT);
|
||||
END LOOP;
|
||||
|
||||
-- List accounts with no vessel created for more than 1 YEAR
|
||||
FOR no_activity_rec in
|
||||
SELECT a.user_id,a.email,a.first,a.created_at
|
||||
FROM auth.accounts a
|
||||
WHERE NOT EXISTS (
|
||||
SELECT *
|
||||
FROM auth.vessels v
|
||||
WHERE v.owner_email = a.email)
|
||||
AND a.created_at < NOW() AT TIME ZONE 'UTC' - INTERVAL '1 YEAR'
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_deactivated_rec_fn for no vessel [%]', no_activity_rec;
|
||||
SELECT json_build_object('email', no_activity_rec.owner_email, 'recipient', no_activity_rec.first) into user_settings;
|
||||
RAISE NOTICE '-> debug cron_process_deactivated_rec_fn no vessel [%]', user_settings;
|
||||
-- Send notification
|
||||
PERFORM send_notification_fn('deactivated'::TEXT, user_settings::JSONB);
|
||||
--PERFORM public.delete_account_fn(no_activity_rec.owner_email::TEXT, no_activity_rec.vessel_id::TEXT);
|
||||
END LOOP;
|
||||
END;
|
||||
$deactivated$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.cron_process_deactivated_fn
|
||||
IS 'init by pg_cron, check for vessel with no activity for more than 1 year then send notification and delete data';
|
||||
|
||||
-- Need to be in the postgres database.
|
||||
\c postgres
|
||||
-- CRON for clean up job details logs
|
||||
CREATE FUNCTION job_run_details_cleanup_fn() RETURNS void AS $$
|
||||
DECLARE
|
||||
BEGIN
|
||||
-- Remove job run log older than 3 months
|
||||
RAISE NOTICE 'job_run_details_cleanup_fn';
|
||||
DELETE FROM cron.job_run_details
|
||||
WHERE start_time <= NOW() AT TIME ZONE 'UTC' - INTERVAL '91 DAYS';
|
||||
END;
|
||||
$$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.job_run_details_cleanup_fn
|
||||
IS 'init by pg_cron to cleanup job_run_details table on schema public postgres db';
|
||||
|
@@ -52,7 +52,7 @@ COMMENT ON TABLE
|
||||
-- with escape value, eg: E'A\nB\r\nC'
|
||||
-- https://stackoverflow.com/questions/26638615/insert-line-break-in-postgresql-when-updating-text-field
|
||||
-- TODO Update notification subject for log entry to 'logbook #NB ...'
|
||||
INSERT INTO email_templates VALUES
|
||||
INSERT INTO public.email_templates VALUES
|
||||
('logbook',
|
||||
'New Logbook Entry',
|
||||
E'Hello __RECIPIENT__,\n\nWe just wanted to let you know that you have a new entry on openplotter.cloud: "__LOGBOOK_NAME__"\r\n\r\nSee more details at __APP_URL__/log/__LOGBOOK_LINK__\n\nHappy sailing!\nThe PostgSail Team',
|
||||
@@ -64,19 +64,19 @@ INSERT INTO email_templates VALUES
|
||||
'Welcome',
|
||||
E'Hi!\nYou successfully created an account\nKeep in mind to register your vessel.\n'),
|
||||
('new_vessel',
|
||||
'New vessel',
|
||||
'New boat',
|
||||
E'Hi!\nHow are you?\n__BOAT__ is now linked to your account.\n',
|
||||
'New vessel',
|
||||
'New boat',
|
||||
E'Hi!\nHow are you?\n__BOAT__ is now linked to your account.\n'),
|
||||
('monitor_offline',
|
||||
'Vessel Offline',
|
||||
'Boat went Offline',
|
||||
E'__BOAT__ has been offline for more than an hour\r\nFind more details at __APP_URL__/boats\n',
|
||||
'Vessel Offline',
|
||||
'Boat went Offline',
|
||||
E'__BOAT__ has been offline for more than an hour\r\nFind more details at __APP_URL__/boats\n'),
|
||||
('monitor_online',
|
||||
'Vessel Online',
|
||||
'Boat went Online',
|
||||
E'__BOAT__ just came online\nFind more details at __APP_URL__/boats\n',
|
||||
'Vessel Online',
|
||||
'Boat went Online',
|
||||
E'__BOAT__ just came online\nFind more details at __APP_URL__/boats\n'),
|
||||
('new_badge',
|
||||
'New Badge!',
|
||||
@@ -115,19 +115,29 @@ INSERT INTO email_templates VALUES
|
||||
E'Congratulations!\nYou have just connect your account to your vessel, @postgsail_bot.\n'),
|
||||
('no_vessel',
|
||||
'PostgSail add your boat',
|
||||
E'Hello __RECIPIENT__,\nYou have created an account on PostgSail but you have not created your boat yet.\nIf you need any assistance we would be happy to help. It is free and an open-source.\nThe PostgSail Team',
|
||||
E'Hello __RECIPIENT__,\nYou created an account on PostgSail but you have not added your boat yet.\nIf you need any assistance, I would be happy to help. It is free and an open-source.\nThe PostgSail Team',
|
||||
'PostgSail next step',
|
||||
E'Hello,\nYou should create your vessel. Check your email!\n'),
|
||||
('no_metadata',
|
||||
'PostgSail connect your boat',
|
||||
E'Hello __RECIPIENT__,\nYou have created an account on PostgSail but you have not connected your boat yet.\nIf you need any assistance we would be happy to help. It is free and an open-source.\nThe PostgSail Team',
|
||||
E'Hello __RECIPIENT__,\nYou created an account on PostgSail but you have not connected your boat yet.\nIf you need any assistance, I would be happy to help. It is free and an open-source.\nThe PostgSail Team',
|
||||
'PostgSail next step',
|
||||
E'Hello,\nYou should connect your vessel. Check your email!\n'),
|
||||
('no_activity',
|
||||
'PostgSail boat inactivity',
|
||||
E'Hello __RECIPIENT__,\nWe don\'t see any activity on your account, do you need any assistance?\nIf you need any assistance we would be happy to help. It is free and an open-source.\nThe PostgSail Team',
|
||||
E'Hello __RECIPIENT__,\nWe don\'t see any activity on your account, do you need any assistance?\nIf you need any assistance, I would be happy to help. It is free and an open-source.\nThe PostgSail Team.',
|
||||
'PostgSail inactivity!',
|
||||
E'Congratulations!\nWe detected inactivity. Check your email!\n');
|
||||
E'We detected inactivity. Check your email!\n'),
|
||||
('deactivated',
|
||||
'PostgSail account deactivated',
|
||||
E'Hello __RECIPIENT__,\nYour account has been deactivated and all your data has been removed from PostgSail system.',
|
||||
'PostgSail deactivated!',
|
||||
E'We removed your account. Check your email!\n'),
|
||||
('grafana',
|
||||
'PostgSail Grafana integration',
|
||||
E'Hello __RECIPIENT__,\nCongratulations! You unlocked Grafana dashboard.\nSee more details at https://app.openplotter.cloud\nHappy sailing!\nFrancois',
|
||||
'PostgSail Grafana!',
|
||||
E'Congratulations!\nYou unlocked Grafana dashboard.\nSee more details at https://app.openplotter.cloud\n');
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Queue handling
|
||||
@@ -145,12 +155,12 @@ INSERT INTO email_templates VALUES
|
||||
|
||||
-- table way
|
||||
CREATE TABLE IF NOT EXISTS public.process_queue (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id INT PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
channel TEXT NOT NULL,
|
||||
payload TEXT NOT NULL,
|
||||
ref_id TEXT NOT NULL,
|
||||
stored TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
processed TIMESTAMP WITHOUT TIME ZONE DEFAULT NULL
|
||||
stored TIMESTAMPTZ NOT NULL,
|
||||
processed TIMESTAMPTZ DEFAULT NULL
|
||||
);
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
@@ -173,7 +183,10 @@ $new_account_entry$ language plpgsql;
|
||||
|
||||
create function new_account_otp_validation_entry_fn() returns trigger as $new_account_otp_validation_entry$
|
||||
begin
|
||||
insert into process_queue (channel, payload, stored, ref_id) values ('email_otp', NEW.email, now(), NEW.user_id);
|
||||
-- Add email_otp check only if not from oauth server
|
||||
if (NEW.preferences->>'email_verified')::boolean IS NOT True then
|
||||
insert into process_queue (channel, payload, stored, ref_id) values ('email_otp', NEW.email, now(), NEW.user_id);
|
||||
end if;
|
||||
return NEW;
|
||||
END;
|
||||
$new_account_otp_validation_entry$ language plpgsql;
|
||||
@@ -185,6 +198,14 @@ begin
|
||||
END;
|
||||
$new_vessel_entry$ language plpgsql;
|
||||
|
||||
create function new_vessel_public_fn() returns trigger as $new_vessel_public$
|
||||
begin
|
||||
-- Update user settings with a public vessel name
|
||||
perform api.update_user_preferences_fn('{public_vessel}', regexp_replace(NEW.name, '\W+', '', 'g'));
|
||||
return NEW;
|
||||
END;
|
||||
$new_vessel_public$ language plpgsql;
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Tables Application Settings
|
||||
-- https://dba.stackexchange.com/questions/27296/storing-application-settings-with-different-datatypes#27297
|
||||
|
@@ -79,9 +79,9 @@ COMMENT ON FUNCTION
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.istimestamptz(text) RETURNS BOOLEAN AS
|
||||
$isdate$
|
||||
DECLARE x TIMESTAMP WITHOUT TIME ZONE;
|
||||
DECLARE x TIMESTAMPTZ;
|
||||
BEGIN
|
||||
x = $1::TIMESTAMP WITHOUT TIME ZONE;
|
||||
x = $1::TIMESTAMPTZ;
|
||||
RETURN TRUE;
|
||||
EXCEPTION WHEN others THEN
|
||||
RETURN FALSE;
|
||||
@@ -92,7 +92,7 @@ LANGUAGE plpgsql IMMUTABLE;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.istimestamptz
|
||||
IS 'Check typeof value is TIMESTAMP WITHOUT TIME ZONE';
|
||||
IS 'Check typeof value is TIMESTAMPTZ';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- JSON helpers
|
||||
@@ -151,3 +151,48 @@ $jsonb_diff_val$ LANGUAGE plpgsql;
|
||||
COMMENT ON FUNCTION
|
||||
public.jsonb_diff_val
|
||||
IS 'Compare two jsonb objects';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- uuid v7 helpers
|
||||
--
|
||||
-- https://gist.github.com/kjmph/5bd772b2c2df145aa645b837da7eca74
|
||||
CREATE OR REPLACE FUNCTION public.timestamp_from_uuid_v7(_uuid uuid)
|
||||
RETURNS timestamp without time zone
|
||||
LANGUAGE sql
|
||||
-- Based off IETF draft, https://datatracker.ietf.org/doc/draft-peabody-dispatch-new-uuid-format/
|
||||
IMMUTABLE PARALLEL SAFE STRICT LEAKPROOF
|
||||
AS $$
|
||||
SELECT to_timestamp(('x0000' || substr(_uuid::text, 1, 8) || substr(_uuid::text, 10, 4))::bit(64)::bigint::numeric / 1000);
|
||||
$$
|
||||
;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.timestamp_from_uuid_v7
|
||||
IS 'extract the timestamp from the uuid.';
|
||||
|
||||
create or replace function public.uuid_generate_v7()
|
||||
returns uuid
|
||||
as $$
|
||||
begin
|
||||
-- use random v4 uuid as starting point (which has the same variant we need)
|
||||
-- then overlay timestamp
|
||||
-- then set version 7 by flipping the 2 and 1 bit in the version 4 string
|
||||
return encode(
|
||||
set_bit(
|
||||
set_bit(
|
||||
overlay(uuid_send(gen_random_uuid())
|
||||
placing substring(int8send(floor(extract(epoch from clock_timestamp()) * 1000)::bigint) from 3)
|
||||
from 1 for 6
|
||||
),
|
||||
52, 1
|
||||
),
|
||||
53, 1
|
||||
),
|
||||
'hex')::uuid;
|
||||
end
|
||||
$$
|
||||
language plpgsql volatile;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.uuid_generate_v7
|
||||
IS 'Generate UUID v7, Based off IETF draft, https://datatracker.ietf.org/doc/draft-peabody-dispatch-new-uuid-format/';
|
||||
|
@@ -15,9 +15,9 @@ CREATE SCHEMA IF NOT EXISTS public;
|
||||
--
|
||||
-- https://github.com/CartoDB/labs-postgresql/blob/master/workshop/plpython.md
|
||||
--
|
||||
DROP FUNCTION IF EXISTS reverse_geocode_py_fn;
|
||||
DROP FUNCTION IF EXISTS reverse_geocode_py_fn;
|
||||
CREATE OR REPLACE FUNCTION reverse_geocode_py_fn(IN geocoder TEXT, IN lon NUMERIC, IN lat NUMERIC,
|
||||
OUT geo jsonb)
|
||||
OUT geo JSONB)
|
||||
AS $reverse_geocode_py$
|
||||
import requests
|
||||
|
||||
@@ -39,47 +39,57 @@ AS $reverse_geocode_py$
|
||||
plpy.error('Error missing parameters')
|
||||
return None
|
||||
|
||||
# Make the request to the geocoder API
|
||||
# https://operations.osmfoundation.org/policies/nominatim/
|
||||
payload = {"lon": lon, "lat": lat, "format": "jsonv2", "zoom": 18}
|
||||
# https://nominatim.org/release-docs/latest/api/Reverse/
|
||||
r = requests.get(url, headers = {"Accept-Language": "en-US,en;q=0.5"}, params=payload)
|
||||
def georeverse(geocoder, lon, lat, zoom="18"):
|
||||
# Make the request to the geocoder API
|
||||
# https://operations.osmfoundation.org/policies/nominatim/
|
||||
headers = {"Accept-Language": "en-US,en;q=0.5", "User-Agent": "PostgSail", "From": "xbgmsharp@gmail.com"}
|
||||
payload = {"lon": lon, "lat": lat, "format": "jsonv2", "zoom": zoom, "accept-language": "en"}
|
||||
# https://nominatim.org/release-docs/latest/api/Reverse/
|
||||
r = requests.get(url, headers=headers, params=payload)
|
||||
|
||||
# Parse response
|
||||
# Option1: If name is null fallback to address field road,neighbourhood,suburb
|
||||
# Option2: Return the json for future reference like country
|
||||
if r.status_code == 200 and "name" in r.json():
|
||||
r_dict = r.json()
|
||||
#plpy.notice('reverse_geocode_py_fn Parameters [{}] [{}] Response'.format(lon, lat, r_dict))
|
||||
output = None
|
||||
country_code = None
|
||||
if "country_code" in r_dict["address"] and r_dict["address"]["country_code"]:
|
||||
country_code = r_dict["address"]["country_code"]
|
||||
if r_dict["name"]:
|
||||
return { "name": r_dict["name"], "country_code": country_code }
|
||||
elif "address" in r_dict and r_dict["address"]:
|
||||
if "neighbourhood" in r_dict["address"] and r_dict["address"]["neighbourhood"]:
|
||||
return { "name": r_dict["address"]["neighbourhood"], "country_code": country_code }
|
||||
elif "road" in r_dict["address"] and r_dict["address"]["road"]:
|
||||
return { "name": r_dict["address"]["road"], "country_code": country_code }
|
||||
elif "suburb" in r_dict["address"] and r_dict["address"]["suburb"]:
|
||||
return { "name": r_dict["address"]["suburb"], "country_code": country_code }
|
||||
elif "residential" in r_dict["address"] and r_dict["address"]["residential"]:
|
||||
return { "name": r_dict["address"]["residential"], "country_code": country_code }
|
||||
elif "village" in r_dict["address"] and r_dict["address"]["village"]:
|
||||
return { "name": r_dict["address"]["village"], "country_code": country_code }
|
||||
elif "town" in r_dict["address"] and r_dict["address"]["town"]:
|
||||
return { "name": r_dict["address"]["town"], "country_code": country_code }
|
||||
else:
|
||||
return { "name": "n/a", "country_code": country_code }
|
||||
else:
|
||||
return { "name": "n/a", "country_code": country_code }
|
||||
else:
|
||||
plpy.warning('Failed to received a geo full address %s', r.json())
|
||||
#plpy.error('Failed to received a geo full address %s', r.json())
|
||||
return { "name": "unknown", "country_code": country_code }
|
||||
# Parse response
|
||||
# If name is null fallback to address field tags: neighbourhood,suburb
|
||||
# if none repeat with lower zoom level
|
||||
if r.status_code == 200 and "name" in r.json():
|
||||
r_dict = r.json()
|
||||
#plpy.notice('reverse_geocode_py_fn Parameters [{}] [{}] Response'.format(lon, lat, r_dict))
|
||||
output = None
|
||||
country_code = None
|
||||
if "country_code" in r_dict["address"] and r_dict["address"]["country_code"]:
|
||||
country_code = r_dict["address"]["country_code"]
|
||||
if r_dict["name"]:
|
||||
return { "name": r_dict["name"], "country_code": country_code }
|
||||
elif "address" in r_dict and r_dict["address"]:
|
||||
if "neighbourhood" in r_dict["address"] and r_dict["address"]["neighbourhood"]:
|
||||
return { "name": r_dict["address"]["neighbourhood"], "country_code": country_code }
|
||||
elif "hamlet" in r_dict["address"] and r_dict["address"]["hamlet"]:
|
||||
return { "name": r_dict["address"]["hamlet"], "country_code": country_code }
|
||||
elif "suburb" in r_dict["address"] and r_dict["address"]["suburb"]:
|
||||
return { "name": r_dict["address"]["suburb"], "country_code": country_code }
|
||||
elif "residential" in r_dict["address"] and r_dict["address"]["residential"]:
|
||||
return { "name": r_dict["address"]["residential"], "country_code": country_code }
|
||||
elif "village" in r_dict["address"] and r_dict["address"]["village"]:
|
||||
return { "name": r_dict["address"]["village"], "country_code": country_code }
|
||||
elif "town" in r_dict["address"] and r_dict["address"]["town"]:
|
||||
return { "name": r_dict["address"]["town"], "country_code": country_code }
|
||||
elif "amenity" in r_dict["address"] and r_dict["address"]["amenity"]:
|
||||
return { "name": r_dict["address"]["amenity"], "country_code": country_code }
|
||||
else:
|
||||
if (zoom == 15):
|
||||
plpy.notice('georeverse recursive retry with lower zoom than:[{}], Response [{}]'.format(zoom , r.json()))
|
||||
return { "name": "n/a", "country_code": country_code }
|
||||
else:
|
||||
plpy.notice('georeverse recursive retry with lower zoom than:[{}], Response [{}]'.format(zoom , r.json()))
|
||||
return georeverse(geocoder, lon, lat, 15)
|
||||
else:
|
||||
return { "name": "n/a", "country_code": country_code }
|
||||
else:
|
||||
plpy.warning('Failed to received a geo full address %s', r.json())
|
||||
#plpy.error('Failed to received a geo full address %s', r.json())
|
||||
return { "name": "unknown", "country_code": "unknown" }
|
||||
|
||||
return georeverse(geocoder, lon, lat)
|
||||
$reverse_geocode_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
|
||||
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.reverse_geocode_py_fn
|
||||
@@ -347,6 +357,10 @@ AS $urlencode_py$
|
||||
import urllib.parse
|
||||
return urllib.parse.quote(uri, safe="");
|
||||
$urlencode_py$ LANGUAGE plpython3u IMMUTABLE STRICT;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.urlencode_py_fn
|
||||
IS 'python url encode using plpython3u';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- python
|
||||
@@ -364,16 +378,14 @@ AS $reverse_geoip_py$
|
||||
url = f'https://ipapi.co/{_ip}/json/'
|
||||
r = requests.get(url)
|
||||
#print(r.text)
|
||||
# Return something boolean?
|
||||
plpy.warning('IP [{}] [{}]'.format(_ip, r.status_code))
|
||||
#plpy.notice('IP [{}] [{}]'.format(_ip, r.status_code))
|
||||
if r.status_code == 200:
|
||||
#plpy.notice('Got [{}] [{}]'.format(r.text, r.status_code))
|
||||
return r.text;
|
||||
return r.json()
|
||||
else:
|
||||
plpy.error('Failed to get ip details')
|
||||
return '{}'
|
||||
$reverse_geoip_py$ LANGUAGE plpython3u;
|
||||
|
||||
return {}
|
||||
$reverse_geoip_py$ IMMUTABLE strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.reverse_geoip_py_fn
|
||||
@@ -422,4 +434,335 @@ IMMUTABLE STRICT;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.geojson_py_fn
|
||||
IS 'Parse geojson using plpython3u (should be done in PGSQL)';
|
||||
IS 'Parse geojson using plpython3u (should be done in PGSQL), deprecated';
|
||||
|
||||
DROP FUNCTION IF EXISTS overpass_py_fn;
|
||||
CREATE OR REPLACE FUNCTION overpass_py_fn(IN lon NUMERIC, IN lat NUMERIC,
|
||||
OUT geo JSONB) RETURNS JSONB
|
||||
AS $overpass_py$
|
||||
"""
|
||||
Return https://overpass-turbo.eu seamark details within 400m
|
||||
https://overpass-turbo.eu/s/1EaG
|
||||
https://wiki.openstreetmap.org/wiki/Key:seamark:type
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com'}
|
||||
payload = """
|
||||
[out:json][timeout:20];
|
||||
is_in({0},{1})->.result_areas;
|
||||
(
|
||||
area.result_areas["seamark:type"~"(mooring|harbour)"][~"^seamark:.*:category$"~"."];
|
||||
area.result_areas["leisure"="marina"][~"name"~"."];
|
||||
);
|
||||
out tags;
|
||||
nwr(around:400.0,{0},{1})->.all;
|
||||
(
|
||||
nwr.all["seamark:type"~"(mooring|harbour)"][~"^seamark:.*:category$"~"."];
|
||||
nwr.all["seamark:type"~"(anchorage|anchor_berth|berth)"];
|
||||
nwr.all["leisure"="marina"];
|
||||
nwr.all["natural"~"(bay|beach)"];
|
||||
);
|
||||
out tags;
|
||||
""".format(lat, lon)
|
||||
data = urllib.parse.quote(payload, safe="");
|
||||
url = f'https://overpass-api.de/api/interpreter?data={data}'.format(data)
|
||||
r = requests.get(url, headers)
|
||||
#print(r.text)
|
||||
#plpy.notice(url)
|
||||
plpy.notice('overpass-api coord lon[{}] lat[{}] [{}]'.format(lon, lat, r.status_code))
|
||||
if r.status_code == 200 and "elements" in r.json():
|
||||
r_dict = r.json()
|
||||
plpy.notice('overpass-api Got [{}]'.format(r_dict["elements"]))
|
||||
if r_dict["elements"]:
|
||||
if "tags" in r_dict["elements"][0] and r_dict["elements"][0]["tags"]:
|
||||
return r_dict["elements"][0]["tags"]; # return the first element
|
||||
return {}
|
||||
else:
|
||||
plpy.notice('overpass-api Failed to get overpass-api details')
|
||||
return {}
|
||||
$overpass_py$ IMMUTABLE strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.overpass_py_fn
|
||||
IS 'Return https://overpass-turbo.eu seamark details within 400m using plpython3u';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Provision Grafana SQL
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION grafana_py_fn(IN _v_name TEXT, IN _v_id TEXT,
|
||||
IN _u_email TEXT, IN app JSONB) RETURNS VOID
|
||||
AS $grafana_py$
|
||||
"""
|
||||
https://grafana.com/docs/grafana/latest/developers/http_api/
|
||||
Create organization base on vessel name
|
||||
Create user base on user email
|
||||
Add user to organization
|
||||
Add data_source to organization
|
||||
Add dashboard to organization
|
||||
Update organization preferences
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import re
|
||||
|
||||
grafana_uri = None
|
||||
if 'app.grafana_admin_uri' in app and app['app.grafana_admin_uri']:
|
||||
grafana_uri = app['app.grafana_admin_uri']
|
||||
else:
|
||||
plpy.error('Error no grafana_admin_uri defined, check app settings')
|
||||
return None
|
||||
|
||||
# add vessel org
|
||||
headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com',
|
||||
'Accept': 'application/json', 'Content-Type': 'application/json'}
|
||||
path = 'api/orgs'
|
||||
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
|
||||
data_dict = {'name':_v_name}
|
||||
data = json.dumps(data_dict)
|
||||
r = requests.post(url, data=data, headers=headers)
|
||||
#print(r.text)
|
||||
plpy.notice(r.json())
|
||||
if r.status_code == 200 and "orgId" in r.json():
|
||||
org_id = r.json()['orgId']
|
||||
else:
|
||||
plpy.error('Error grafana add vessel org %', r.json())
|
||||
return None
|
||||
|
||||
# add user to vessel org
|
||||
path = 'api/admin/users'
|
||||
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
|
||||
data_dict = {'orgId':org_id, 'email':_u_email, 'password':'asupersecretpassword'}
|
||||
data = json.dumps(data_dict)
|
||||
r = requests.post(url, data=data, headers=headers)
|
||||
#print(r.text)
|
||||
plpy.notice(r.json())
|
||||
if r.status_code == 200 and "id" in r.json():
|
||||
user_id = r.json()['id']
|
||||
else:
|
||||
plpy.error('Error grafana add user to vessel org')
|
||||
return
|
||||
|
||||
# read data_source
|
||||
path = 'api/datasources/1'
|
||||
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
|
||||
r = requests.get(url, headers=headers)
|
||||
#print(r.text)
|
||||
plpy.notice(r.json())
|
||||
data_source = r.json()
|
||||
data_source['id'] = 0
|
||||
data_source['orgId'] = org_id
|
||||
data_source['uid'] = "ds_" + _v_id
|
||||
data_source['name'] = "ds_" + _v_id
|
||||
data_source['secureJsonData'] = {}
|
||||
data_source['secureJsonData']['password'] = 'password'
|
||||
data_source['readOnly'] = True
|
||||
del data_source['secureJsonFields']
|
||||
|
||||
# add data_source to vessel org
|
||||
path = 'api/datasources'
|
||||
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
|
||||
data = json.dumps(data_source)
|
||||
headers['X-Grafana-Org-Id'] = str(org_id)
|
||||
r = requests.post(url, data=data, headers=headers)
|
||||
plpy.notice(r.json())
|
||||
del headers['X-Grafana-Org-Id']
|
||||
if r.status_code != 200 and "id" not in r.json():
|
||||
plpy.error('Error grafana add data_source to vessel org')
|
||||
return
|
||||
|
||||
dashboards_tpl = [ 'pgsail_tpl_electrical', 'pgsail_tpl_logbook', 'pgsail_tpl_monitor', 'pgsail_tpl_rpi', 'pgsail_tpl_solar', 'pgsail_tpl_weather', 'pgsail_tpl_home']
|
||||
for dashboard in dashboards_tpl:
|
||||
# read dashboard template by uid
|
||||
path = 'api/dashboards/uid'
|
||||
url = f'{grafana_uri}/{path}/{dashboard}'.format(grafana_uri,path,dashboard)
|
||||
if 'X-Grafana-Org-Id' in headers:
|
||||
del headers['X-Grafana-Org-Id']
|
||||
r = requests.get(url, headers=headers)
|
||||
plpy.notice(r.json())
|
||||
if r.status_code != 200 and "id" not in r.json():
|
||||
plpy.error('Error grafana read dashboard template')
|
||||
return
|
||||
new_dashboard = r.json()
|
||||
del new_dashboard['meta']
|
||||
new_dashboard['dashboard']['version'] = 0
|
||||
new_dashboard['dashboard']['id'] = 0
|
||||
new_uid = re.sub(r'pgsail_tpl_(.*)', r'postgsail_\1', new_dashboard['dashboard']['uid'])
|
||||
new_dashboard['dashboard']['uid'] = f'{new_uid}_{_v_id}'.format(new_uid,_v_id)
|
||||
# add dashboard to vessel org
|
||||
path = 'api/dashboards/db'
|
||||
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
|
||||
data = json.dumps(new_dashboard)
|
||||
new_data = data.replace('PCC52D03280B7034C', data_source['uid'])
|
||||
headers['X-Grafana-Org-Id'] = str(org_id)
|
||||
r = requests.post(url, data=new_data, headers=headers)
|
||||
plpy.notice(r.json())
|
||||
if r.status_code != 200 and "id" not in r.json():
|
||||
plpy.error('Error grafana add dashboard to vessel org')
|
||||
return
|
||||
|
||||
# Update Org Prefs
|
||||
path = 'api/org/preferences'
|
||||
url = f'{grafana_uri}/{path}'.format(grafana_uri,path)
|
||||
home_dashboard = {}
|
||||
home_dashboard['timezone'] = 'utc'
|
||||
home_dashboard['homeDashboardUID'] = f'postgsail_home_{_v_id}'.format(_v_id)
|
||||
data = json.dumps(home_dashboard)
|
||||
headers['X-Grafana-Org-Id'] = str(org_id)
|
||||
r = requests.patch(url, data=data, headers=headers)
|
||||
plpy.notice(r.json())
|
||||
if r.status_code != 200:
|
||||
plpy.error('Error grafana update org preferences')
|
||||
return
|
||||
|
||||
plpy.notice('Done')
|
||||
$grafana_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.grafana_py_fn
|
||||
IS 'Grafana Organization,User,data_source,dashboards provisioning via HTTP API using plpython3u';
|
||||
|
||||
-- https://stackoverflow.com/questions/65517230/how-to-set-user-attribute-value-in-keycloak-using-api
|
||||
DROP FUNCTION IF EXISTS keycloak_py_fn;
|
||||
CREATE OR REPLACE FUNCTION keycloak_py_fn(IN user_id TEXT, IN vessel_id TEXT,
|
||||
IN app JSONB) RETURNS JSONB
|
||||
AS $keycloak_py$
|
||||
"""
|
||||
Add vessel_id user attribute to keycloak user {user_id}
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
safe_uri = host = user = pwd = None
|
||||
if 'app.keycloak_uri' in app and app['app.keycloak_uri']:
|
||||
#safe_uri = urllib.parse.quote(app['app.keycloak_uri'], safe=':/?&=')
|
||||
_ = urllib.parse.urlparse(app['app.keycloak_uri'])
|
||||
host = _.netloc.split('@')[-1]
|
||||
user = _.netloc.split(':')[0]
|
||||
pwd = _.netloc.split(':')[1].split('@')[0]
|
||||
else:
|
||||
plpy.error('Error no keycloak_uri defined, check app settings')
|
||||
return None
|
||||
|
||||
if not host or not user or not pwd:
|
||||
plpy.error('Error parsing keycloak_uri, check app settings')
|
||||
return None
|
||||
|
||||
_headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com'}
|
||||
_payload = {'client_id':'admin-cli','grant_type':'password','username':user,'password':pwd}
|
||||
url = f'{_.scheme}://{host}/realms/master/protocol/openid-connect/token'.format(_.scheme, host)
|
||||
r = requests.post(url, headers=_headers, data=_payload, timeout=(5, 60))
|
||||
#print(r.text)
|
||||
#plpy.notice(url)
|
||||
if r.status_code == 200 and 'access_token' in r.json():
|
||||
response = r.json()
|
||||
plpy.notice(response)
|
||||
_headers['Authorization'] = 'Bearer '+ response['access_token']
|
||||
_headers['Content-Type'] = 'application/json'
|
||||
_payload = { 'attributes': {'vessel_id': vessel_id} }
|
||||
url = f'{keycloak_uri}/admin/realms/postgsail/users/{user_id}'.format(keycloak_uri,user_id)
|
||||
#plpy.notice(url)
|
||||
#plpy.notice(_payload)
|
||||
data = json.dumps(_payload)
|
||||
r = requests.put(url, headers=_headers, data=data, timeout=(5, 60))
|
||||
if r.status_code != 204:
|
||||
plpy.notice("Error updating user: {status} [{text}]".format(
|
||||
status=r.status_code, text=r.text))
|
||||
return None
|
||||
else:
|
||||
plpy.notice("Updated user : {user} [{text}]".format(user=user_id, text=r.text))
|
||||
else:
|
||||
plpy.notice(f'Error getting admin access_token: {status} [{text}]'.format(
|
||||
status=r.status_code, text=r.text))
|
||||
return None
|
||||
$keycloak_py$ strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.keycloak_py_fn
|
||||
IS 'Return set oauth user attribute into keycloak using plpython3u';
|
||||
|
||||
DROP FUNCTION IF EXISTS keycloak_auth_py_fn;
|
||||
CREATE OR REPLACE FUNCTION keycloak_auth_py_fn(IN _v_id TEXT,
|
||||
IN _user JSONB, IN app JSONB) RETURNS JSONB
|
||||
AS $keycloak_auth_py$
|
||||
"""
|
||||
Addkeycloak user
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
import urllib.parse
|
||||
|
||||
safe_uri = host = user = pwd = None
|
||||
if 'app.keycloak_uri' in app and app['app.keycloak_uri']:
|
||||
#safe_uri = urllib.parse.quote(app['app.keycloak_uri'], safe=':/?&=')
|
||||
_ = urllib.parse.urlparse(app['app.keycloak_uri'])
|
||||
host = _.netloc.split('@')[-1]
|
||||
user = _.netloc.split(':')[0]
|
||||
pwd = _.netloc.split(':')[1].split('@')[0]
|
||||
else:
|
||||
plpy.error('Error no keycloak_uri defined, check app settings')
|
||||
return none
|
||||
|
||||
if not host or not user or not pwd:
|
||||
plpy.error('Error parsing keycloak_uri, check app settings')
|
||||
return None
|
||||
|
||||
if not 'email' in _user and _user['email']:
|
||||
plpy.error('Error parsing user email, check user settings')
|
||||
return none
|
||||
|
||||
if not _v_id:
|
||||
plpy.error('Error parsing vessel_id')
|
||||
return none
|
||||
|
||||
_headers = {'User-Agent': 'PostgSail', 'From': 'xbgmsharp@gmail.com'}
|
||||
_payload = {'client_id':'admin-cli','grant_type':'password','username':user,'password':pwd}
|
||||
url = f'{_.scheme}://{host}/realms/master/protocol/openid-connect/token'.format(_.scheme, host)
|
||||
r = requests.post(url, headers=_headers, data=_payload, timeout=(5, 60))
|
||||
#print(r.text)
|
||||
#plpy.notice(url)
|
||||
if r.status_code == 200 and 'access_token' in r.json():
|
||||
response = r.json()
|
||||
plpy.notice(response)
|
||||
_headers['Authorization'] = 'Bearer '+ response['access_token']
|
||||
_headers['Content-Type'] = 'application/json'
|
||||
url = f'{_.scheme}://{host}/admin/realms/postgsail/users'.format(_.scheme, host)
|
||||
_payload = {
|
||||
"enabled": "true",
|
||||
"email": _user['email'],
|
||||
"firstName": _user['recipient'],
|
||||
"attributes": {"vessel_id": _v_id},
|
||||
"emailVerified": True,
|
||||
"requiredActions":["UPDATE_PROFILE", "UPDATE_PASSWORD"]
|
||||
}
|
||||
plpy.notice(_payload)
|
||||
data = json.dumps(_payload)
|
||||
r = requests.post(url, headers=_headers, data=data, timeout=(5, 60))
|
||||
if r.status_code != 201:
|
||||
#print("Error creating user: {status}".format(status=r.status_code))
|
||||
plpy.error(f'Error creating user: {user} {status}'.format(user=_payload['email'], status=r.status_code))
|
||||
return None
|
||||
else:
|
||||
#print("Created user : {u}]".format(u=_payload['email']))
|
||||
plpy.notice('Created user : {u} {t}, {l}'.format(u=_payload['email'], t=r.text, l=r.headers['location']))
|
||||
user_url = "{user_url}/execute-actions-email".format(user_url=r.headers['location'])
|
||||
_payload = ["UPDATE_PASSWORD"]
|
||||
plpy.notice(_payload)
|
||||
data = json.dumps(_payload)
|
||||
r = requests.put(user_url, headers=_headers, data=data, timeout=(5, 60))
|
||||
if r.status_code != 204:
|
||||
plpy.error('Error execute-actions-email: {u} {s}'.format(u=_user['email'], s=r.status_code))
|
||||
else:
|
||||
plpy.notice('execute-actions-email: {u} {s}'.format(u=_user['email'], s=r.status_code))
|
||||
return None
|
||||
else:
|
||||
plpy.error(f'Error getting admin access_token: {status}'.format(status=r.status_code))
|
||||
return None
|
||||
$keycloak_auth_py$ strict TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.keycloak_auth_py_fn
|
||||
IS 'Return set oauth user attribute into keycloak using plpython3u';
|
||||
|
@@ -21,17 +21,18 @@ CREATE EXTENSION IF NOT EXISTS "pgcrypto"; -- provides cryptographic functions
|
||||
|
||||
DROP TABLE IF EXISTS auth.accounts CASCADE;
|
||||
CREATE TABLE IF NOT EXISTS auth.accounts (
|
||||
userid UUID NOT NULL UNIQUE DEFAULT uuid_generate_v4(),
|
||||
id INT UNIQUE GENERATED ALWAYS AS IDENTITY,
|
||||
--id TEXT NOT NULL UNIQUE DEFAULT uuid_generate_v7(),
|
||||
user_id TEXT NOT NULL UNIQUE DEFAULT RIGHT(gen_random_uuid()::text, 12),
|
||||
email CITEXT primary key check ( email ~* '^.+@.+\..+$' ),
|
||||
first text not null check (length(pass) < 512),
|
||||
last text not null check (length(pass) < 512),
|
||||
pass text not null check (length(pass) < 512),
|
||||
role name not null check (length(role) < 512),
|
||||
email CITEXT PRIMARY KEY CHECK ( email ~* '^.+@.+\..+$' ),
|
||||
first TEXT NOT NULL CHECK (length(pass) < 512),
|
||||
last TEXT NOT NULL CHECK (length(pass) < 512),
|
||||
pass TEXT NOT NULL CHECK (length(pass) < 512),
|
||||
role name NOT NULL CHECK (length(role) < 512),
|
||||
preferences JSONB NULL DEFAULT '{"email_notifications":true}',
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
connected_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
connected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT valid_email CHECK (length(email) > 5), -- Enforce at least 5 char, eg: a@b.io
|
||||
CONSTRAINT valid_first CHECK (length(first) > 1),
|
||||
CONSTRAINT valid_last CHECK (length(last) > 1),
|
||||
@@ -42,11 +43,9 @@ COMMENT ON TABLE
|
||||
auth.accounts
|
||||
IS 'users account table';
|
||||
-- Indexes
|
||||
-- is unused index?
|
||||
--CREATE INDEX accounts_role_idx ON auth.accounts (role);
|
||||
CREATE INDEX accounts_preferences_idx ON auth.accounts using GIN (preferences);
|
||||
-- is unused index?
|
||||
--CREATE INDEX accounts_userid_idx ON auth.accounts (userid);
|
||||
CREATE INDEX accounts_preferences_idx ON auth.accounts USING GIN (preferences);
|
||||
COMMENT ON COLUMN auth.accounts.first IS 'User first name with CONSTRAINT CHECK';
|
||||
COMMENT ON COLUMN auth.accounts.last IS 'User last name with CONSTRAINT CHECK';
|
||||
|
||||
CREATE TRIGGER accounts_moddatetime
|
||||
BEFORE UPDATE ON auth.accounts
|
||||
@@ -59,39 +58,59 @@ COMMENT ON TRIGGER accounts_moddatetime
|
||||
|
||||
DROP TABLE IF EXISTS auth.vessels;
|
||||
CREATE TABLE IF NOT EXISTS auth.vessels (
|
||||
vessel_id TEXT NOT NULL UNIQUE DEFAULT RIGHT(gen_random_uuid()::text, 12),
|
||||
vessel_id TEXT NOT NULL UNIQUE DEFAULT RIGHT(gen_random_uuid()::text, 12),
|
||||
-- user_id TEXT NOT NULL REFERENCES auth.accounts(user_id) ON DELETE RESTRICT,
|
||||
owner_email CITEXT PRIMARY KEY REFERENCES auth.accounts(email) ON DELETE RESTRICT,
|
||||
-- mmsi TEXT UNIQUE, -- Should be a numeric range between 100000000 and 800000000.
|
||||
mmsi NUMERIC UNIQUE, -- MMSI can be optional but if present must be a valid one and unique
|
||||
name TEXT NOT NULL CHECK (length(name) >= 3 AND length(name) < 512),
|
||||
-- pass text not null check (length(pass) < 512), -- unused
|
||||
role name not null check (length(role) < 512),
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW()
|
||||
-- CONSTRAINT valid_length_mmsi CHECK (length(mmsi) < 10 OR length(mmsi) = 0)
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
CONSTRAINT valid_range_mmsi CHECK (mmsi > 100000000 AND mmsi < 800000000)
|
||||
);
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
auth.vessels
|
||||
IS 'vessels table link to accounts email user_id column';
|
||||
-- Indexes
|
||||
-- is unused index?
|
||||
--CREATE INDEX vessels_role_idx ON auth.vessels (role);
|
||||
-- is unused index?
|
||||
--CREATE INDEX vessels_name_idx ON auth.vessels (name);
|
||||
CREATE INDEX vessels_vesselid_idx ON auth.vessels (vessel_id);
|
||||
COMMENT ON COLUMN
|
||||
auth.vessels.mmsi
|
||||
IS 'MMSI can be optional but if present must be a valid one and unique but must be in numeric range between 100000000 and 800000000';
|
||||
|
||||
CREATE TRIGGER vessels_moddatetime
|
||||
BEFORE UPDATE ON auth.vessels
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE moddatetime (updated_at);
|
||||
BEFORE UPDATE ON auth.vessels
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE moddatetime (updated_at);
|
||||
-- Description
|
||||
COMMENT ON TRIGGER vessels_moddatetime
|
||||
ON auth.vessels
|
||||
IS 'Automatic update of updated_at on table modification';
|
||||
|
||||
CREATE TABLE auth.users (
|
||||
id NAME PRIMARY KEY DEFAULT current_setting('request.jwt.claims', true)::json->>'sub',
|
||||
email NAME NOT NULL DEFAULT current_setting('request.jwt.claims', true)::json->>'email',
|
||||
user_id TEXT NOT NULL UNIQUE DEFAULT RIGHT(gen_random_uuid()::text, 12),
|
||||
first TEXT NOT NULL DEFAULT current_setting('request.jwt.claims', true)::json->>'given_name',
|
||||
last TEXT NOT NULL DEFAULT current_setting('request.jwt.claims', true)::json->>'family_name',
|
||||
role NAME NOT NULL DEFAULT 'user_role' CHECK (length(role) < 512),
|
||||
preferences JSONB NULL DEFAULT '{"email_notifications":true, "email_valid": true, "email_verified": true}',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
connected_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
auth.users
|
||||
IS 'Keycloak Oauth user, map user details from access token';
|
||||
|
||||
CREATE TRIGGER user_moddatetime
|
||||
BEFORE UPDATE ON auth.users
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE moddatetime (updated_at);
|
||||
-- Description
|
||||
COMMENT ON TRIGGER user_moddatetime
|
||||
ON auth.users
|
||||
IS 'Automatic update of updated_at on table modification';
|
||||
|
||||
create or replace function
|
||||
auth.check_role_exists() returns trigger as $$
|
||||
begin
|
||||
@@ -110,12 +129,26 @@ create constraint trigger ensure_user_role_exists
|
||||
after insert or update on auth.accounts
|
||||
for each row
|
||||
execute procedure auth.check_role_exists();
|
||||
-- Description
|
||||
COMMENT ON TRIGGER ensure_user_role_exists
|
||||
ON auth.accounts
|
||||
IS 'ensure user role exists';
|
||||
|
||||
-- trigger add queue new account
|
||||
CREATE TRIGGER new_account_entry AFTER INSERT ON auth.accounts
|
||||
FOR EACH ROW EXECUTE FUNCTION public.new_account_entry_fn();
|
||||
-- Description
|
||||
COMMENT ON TRIGGER new_account_entry
|
||||
ON auth.accounts
|
||||
IS 'Add new account in process_queue for further processing';
|
||||
|
||||
-- trigger add queue new account OTP validation
|
||||
CREATE TRIGGER new_account_otp_validation_entry AFTER INSERT ON auth.accounts
|
||||
FOR EACH ROW EXECUTE FUNCTION public.new_account_otp_validation_entry_fn();
|
||||
-- Description
|
||||
COMMENT ON TRIGGER new_account_otp_validation_entry
|
||||
ON auth.accounts
|
||||
IS 'Add new account OTP validation in process_queue for further processing';
|
||||
|
||||
-- trigger check role on vessel
|
||||
drop trigger if exists ensure_vessel_role_exists on auth.vessels;
|
||||
@@ -126,6 +159,18 @@ create constraint trigger ensure_vessel_role_exists
|
||||
-- trigger add queue new vessel
|
||||
CREATE TRIGGER new_vessel_entry AFTER INSERT ON auth.vessels
|
||||
FOR EACH ROW EXECUTE FUNCTION public.new_vessel_entry_fn();
|
||||
-- Description
|
||||
COMMENT ON TRIGGER new_vessel_entry
|
||||
ON auth.vessels
|
||||
IS 'Add new vessel in process_queue for further processing';
|
||||
|
||||
-- trigger add new vessel name as public_vessel user configuration
|
||||
CREATE TRIGGER new_vessel_public AFTER INSERT ON auth.vessels
|
||||
FOR EACH ROW EXECUTE FUNCTION public.new_vessel_public_fn();
|
||||
-- Description
|
||||
COMMENT ON TRIGGER new_vessel_public
|
||||
ON auth.vessels
|
||||
IS 'Add new vessel name as public_vessel user configuration';
|
||||
|
||||
create or replace function
|
||||
auth.encrypt_pass() returns trigger as $$
|
||||
@@ -243,6 +288,96 @@ begin
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- API account Oauth functions
|
||||
--
|
||||
-- oauth is on your exposed schema
|
||||
create or replace function
|
||||
api.oauth() returns void as $$
|
||||
declare
|
||||
_exist boolean;
|
||||
begin
|
||||
-- Ensure we have the required key/value in the access token
|
||||
if current_setting('request.jwt.claims', true)::json->>'sub' is null OR
|
||||
current_setting('request.jwt.claims', true)::json->>'email' is null THEN
|
||||
return;
|
||||
end if;
|
||||
-- check email exist
|
||||
select exists( select email from auth.users
|
||||
where id = current_setting('request.jwt.claims', true)::json->>'sub'
|
||||
) INTO _exist;
|
||||
if NOT FOUND then
|
||||
RAISE WARNING 'Register new oauth user email:[%]', current_setting('request.jwt.claims', true)::json->>'email';
|
||||
-- insert new user, default value from the oauth access token
|
||||
INSERT INTO auth.users (role, preferences)
|
||||
VALUES ('user_role', '{"email_notifications":true, "email_valid": true, "email_verified": true}');
|
||||
end if;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.oauth
|
||||
IS 'openid/oauth user register entry point';
|
||||
|
||||
create or replace function
|
||||
api.oauth_vessel(in _mmsi text, in _name text) returns void as $$
|
||||
declare
|
||||
_exist boolean;
|
||||
vessel_name text := _name;
|
||||
vessel_mmsi text := _mmsi;
|
||||
_vessel_id text := null;
|
||||
vessel_rec record;
|
||||
app_settings jsonb;
|
||||
_user_id text := null;
|
||||
begin
|
||||
RAISE WARNING 'oauth_vessel:[%]', current_setting('user.email', true);
|
||||
RAISE WARNING 'oauth_vessel:[%]', current_setting('request.jwt.claims', true)::json->>'email';
|
||||
-- Ensure we have the required key/value in the access token
|
||||
if current_setting('request.jwt.claims', true)::json->>'sub' is null OR
|
||||
current_setting('request.jwt.claims', true)::json->>'email' is null THEN
|
||||
return;
|
||||
end if;
|
||||
|
||||
-- check email exist
|
||||
select exists( select email from auth.accounts
|
||||
where email = current_setting('request.jwt.claims', true)::json->>'email'
|
||||
) INTO _exist;
|
||||
if _exist is False then
|
||||
RAISE WARNING 'Register new oauth user email:[%]', current_setting('request.jwt.claims', true)::json->>'email';
|
||||
-- insert new user, default value from the oauth access token
|
||||
INSERT INTO auth.users VALUES(DEFAULT) RETURNING user_id INTO _user_id;
|
||||
-- insert new user to account table from the oauth access token
|
||||
INSERT INTO auth.accounts (email, first, last, pass, user_id, role, preferences)
|
||||
VALUES (current_setting('request.jwt.claims', true)::json->>'email',
|
||||
current_setting('request.jwt.claims', true)::json->>'given_name',
|
||||
current_setting('request.jwt.claims', true)::json->>'family_name',
|
||||
current_setting('request.jwt.claims', true)::json->>'sub',
|
||||
_user_id, 'user_role', '{"email_notifications":true, "email_valid": true, "email_verified": true}');
|
||||
end if;
|
||||
|
||||
IF public.isnumeric(vessel_mmsi) IS False THEN
|
||||
vessel_mmsi = NULL;
|
||||
END IF;
|
||||
-- check vessel exist
|
||||
SELECT * INTO vessel_rec
|
||||
FROM auth.vessels vessel
|
||||
WHERE vessel.owner_email = current_setting('request.jwt.claims', true)::json->>'email';
|
||||
IF vessel_rec IS NULL THEN
|
||||
RAISE WARNING 'Register new vessel name:[%] mmsi:[%] for [%]', vessel_name, vessel_mmsi, current_setting('request.jwt.claims', true)::json->>'email';
|
||||
INSERT INTO auth.vessels (owner_email, mmsi, name, role)
|
||||
VALUES (current_setting('request.jwt.claims', true)::json->>'email', vessel_mmsi::NUMERIC, vessel_name, 'vessel_role') RETURNING vessel_id INTO _vessel_id;
|
||||
-- Gather url from app settings
|
||||
app_settings := get_app_settings_fn();
|
||||
-- set oauth user vessel_id attributes for token generation
|
||||
PERFORM keycloak_py_fn(current_setting('request.jwt.claims', true)::json->>'sub'::TEXT, _vessel_id::TEXT, app_settings);
|
||||
END IF;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.oauth_vessel
|
||||
IS 'user and vessel register entry point from signalk plugin';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- API vessel helper functions
|
||||
-- register_vessel should be on your exposed schema
|
||||
@@ -269,7 +404,7 @@ begin
|
||||
IF vessel_rec IS NULL THEN
|
||||
RAISE WARNING 'Register new vessel name:[%] mmsi:[%] for [%]', vessel_name, vessel_mmsi, vessel_email;
|
||||
INSERT INTO auth.vessels (owner_email, mmsi, name, role)
|
||||
VALUES (vessel_email, vessel_mmsi::NUMERIC, vessel_name, 'vessel_role') RETURNING vessel_id INTO _vessel_id;
|
||||
VALUES (vessel_email, vessel_mmsi::NUMERIC, vessel_name, 'vessel_role') RETURNING vessel_id INTO _vessel_id;
|
||||
vessel_rec.role := 'vessel_role';
|
||||
vessel_rec.owner_email = vessel_email;
|
||||
vessel_rec.vessel_id = _vessel_id;
|
||||
|
@@ -20,12 +20,21 @@ COMMENT ON COLUMN api.metadata.vessel_id IS 'Link auth.vessels with api.metadata
|
||||
|
||||
-- REFERENCE ship type with AIS type ?
|
||||
-- REFERENCE mmsi MID with country ?
|
||||
|
||||
ALTER TABLE api.logbook ADD FOREIGN KEY (_from_moorage_id) REFERENCES api.moorages(id) ON DELETE RESTRICT;
|
||||
COMMENT ON COLUMN api.logbook._from_moorage_id IS 'Link api.moorages with api.logbook via FOREIGN KEY and REFERENCES';
|
||||
ALTER TABLE api.logbook ADD FOREIGN KEY (_to_moorage_id) REFERENCES api.moorages(id) ON DELETE RESTRICT;
|
||||
COMMENT ON COLUMN api.logbook._to_moorage_id IS 'Link api.moorages with api.logbook via FOREIGN KEY and REFERENCES';
|
||||
ALTER TABLE api.stays ADD FOREIGN KEY (moorage_id) REFERENCES api.moorages(id) ON DELETE RESTRICT;
|
||||
COMMENT ON COLUMN api.stays.moorage_id IS 'Link api.moorages with api.stays via FOREIGN KEY and REFERENCES';
|
||||
ALTER TABLE api.stays ADD FOREIGN KEY (stay_code) REFERENCES api.stays_at(stay_code) ON DELETE RESTRICT;
|
||||
COMMENT ON COLUMN api.stays.stay_code IS 'Link api.stays_at with api.stays via FOREIGN KEY and REFERENCES';
|
||||
ALTER TABLE api.moorages ADD FOREIGN KEY (stay_code) REFERENCES api.stays_at(stay_code) ON DELETE RESTRICT;
|
||||
COMMENT ON COLUMN api.moorages.stay_code IS 'Link api.stays_at with api.moorages via FOREIGN KEY and REFERENCES';
|
||||
|
||||
-- List vessel
|
||||
--TODO add geojson with position
|
||||
DROP VIEW IF EXISTS api.vessels_view;
|
||||
CREATE OR REPLACE VIEW api.vessels_view AS
|
||||
CREATE OR REPLACE VIEW api.vessels_view WITH (security_invoker=true,security_barrier=true) AS
|
||||
WITH metadata AS (
|
||||
SELECT COALESCE(
|
||||
(SELECT m.time
|
||||
@@ -37,10 +46,10 @@ CREATE OR REPLACE VIEW api.vessels_view AS
|
||||
SELECT
|
||||
v.name as name,
|
||||
v.mmsi as mmsi,
|
||||
v.created_at::timestamp(0) as created_at,
|
||||
v.created_at as created_at,
|
||||
m.last_contact as last_contact,
|
||||
((NOW() AT TIME ZONE 'UTC' - m.last_contact::timestamp without time zone) > INTERVAL '70 MINUTES') as offline,
|
||||
(NOW() AT TIME ZONE 'UTC' - m.last_contact::timestamp without time zone) as duration
|
||||
((NOW() AT TIME ZONE 'UTC' - m.last_contact::TIMESTAMPTZ) > INTERVAL '70 MINUTES') as offline,
|
||||
(NOW() AT TIME ZONE 'UTC' - m.last_contact::TIMESTAMPTZ) as duration
|
||||
FROM auth.vessels v, metadata m
|
||||
WHERE v.owner_email = current_setting('user.email');
|
||||
-- Description
|
||||
@@ -96,9 +105,10 @@ AS $vessel$
|
||||
BEGIN
|
||||
SELECT
|
||||
jsonb_build_object(
|
||||
'name', v.name,
|
||||
'mmsi', coalesce(v.mmsi, null),
|
||||
'created_at', v.created_at::timestamp(0),
|
||||
'name', coalesce(m.name, null),
|
||||
'mmsi', coalesce(m.mmsi, null),
|
||||
'created_at', v.created_at,
|
||||
'first_contact', coalesce(m.created_at, null),
|
||||
'last_contact', coalesce(m.time, null),
|
||||
'geojson', coalesce(ST_AsGeoJSON(geojson_t.*)::json, null)
|
||||
)::jsonb || api.vessel_details_fn()::jsonb
|
||||
@@ -117,7 +127,7 @@ AS $vessel$
|
||||
latitude IS NOT NULL
|
||||
AND longitude IS NOT NULL
|
||||
AND vessel_id = current_setting('vessel.id', false)
|
||||
ORDER BY time DESC
|
||||
ORDER BY time DESC LIMIT 1
|
||||
) AS geojson_t
|
||||
WHERE
|
||||
m.vessel_id = current_setting('vessel.id')
|
||||
@@ -136,14 +146,14 @@ CREATE OR REPLACE FUNCTION api.settings_fn(out settings json) RETURNS JSON
|
||||
AS $user_settings$
|
||||
BEGIN
|
||||
select row_to_json(row)::json INTO settings
|
||||
from (
|
||||
select a.email, a.first, a.last, a.preferences, a.created_at,
|
||||
from (
|
||||
select a.email, a.first, a.last, a.preferences, a.created_at,
|
||||
INITCAP(CONCAT (LEFT(first, 1), ' ', last)) AS username,
|
||||
public.has_vessel_fn() as has_vessel
|
||||
--public.has_vessel_metadata_fn() as has_vessel_metadata,
|
||||
from auth.accounts a
|
||||
where email = current_setting('user.email')
|
||||
) row;
|
||||
) row;
|
||||
END;
|
||||
$user_settings$ language plpgsql security definer;
|
||||
-- Description
|
||||
@@ -232,16 +242,17 @@ $vessel_details$
|
||||
DECLARE
|
||||
BEGIN
|
||||
RETURN ( WITH tbl AS (
|
||||
SELECT mmsi,ship_type,length,beam,height,plugin_version FROM api.metadata WHERE vessel_id = current_setting('vessel.id', false)
|
||||
SELECT mmsi,ship_type,length,beam,height,plugin_version,platform FROM api.metadata WHERE vessel_id = current_setting('vessel.id', false)
|
||||
)
|
||||
SELECT json_build_object(
|
||||
'ship_type', (SELECT ais.description FROM aistypes ais, tbl t WHERE t.ship_type = ais.id),
|
||||
'country', (SELECT mid.country FROM mid, tbl t WHERE LEFT(cast(t.mmsi as text), 3)::NUMERIC = mid.id),
|
||||
'alpha_2', (SELECT o.alpha_2 FROM mid m, iso3166 o, tbl t WHERE LEFT(cast(t.mmsi as text), 3)::NUMERIC = m.id AND m.country_id = o.id),
|
||||
'length', t.ship_type,
|
||||
'length', t.length,
|
||||
'beam', t.beam,
|
||||
'height', t.height,
|
||||
'plugin_version', t.plugin_version)
|
||||
'plugin_version', t.plugin_version,
|
||||
'platform', t.platform)
|
||||
FROM tbl t
|
||||
);
|
||||
END;
|
||||
@@ -254,10 +265,10 @@ COMMENT ON FUNCTION
|
||||
DROP VIEW IF EXISTS api.eventlogs_view;
|
||||
CREATE VIEW api.eventlogs_view WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT pq.*
|
||||
from public.process_queue pq
|
||||
where ref_id = current_setting('user.id', true)
|
||||
or ref_id = current_setting('vessel.id', true)
|
||||
order by id asc;
|
||||
FROM public.process_queue pq
|
||||
WHERE channel <> 'pre_logbook' AND (ref_id = current_setting('user.id', true)
|
||||
OR ref_id = current_setting('vessel.id', true))
|
||||
ORDER BY id ASC;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
api.eventlogs_view
|
||||
@@ -268,20 +279,78 @@ DROP FUNCTION IF EXISTS api.update_logbook_observations_fn;
|
||||
CREATE OR REPLACE FUNCTION api.update_logbook_observations_fn(IN _id INT, IN observations TEXT) RETURNS BOOLEAN AS
|
||||
$update_logbook_observations$
|
||||
DECLARE
|
||||
_value TEXT := NULL;
|
||||
BEGIN
|
||||
-- Merge existing observations with the new observations objects
|
||||
RAISE NOTICE '-> update_logbook_extra_fn id:[%] observations:[%]', _id, observations;
|
||||
_value := to_jsonb(observations)::jsonb;
|
||||
-- { 'observations': { 'seaState': -1, 'cloudCoverage': -1, 'visibility': -1 } }
|
||||
UPDATE api.logbook SET extra = public.jsonb_recursive_merge(extra, _value) WHERE id = _id;
|
||||
UPDATE api.logbook SET extra = public.jsonb_recursive_merge(extra, observations::jsonb) WHERE id = _id;
|
||||
IF FOUND IS True THEN
|
||||
RETURN True;
|
||||
END IF;
|
||||
RETURN False;
|
||||
END;
|
||||
$update_logbook_observations$ language plpgsql security definer;
|
||||
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.update_logbook_observations_fn
|
||||
IS 'Update logbook observations jsonb key pair value';
|
||||
IS 'Update/Add logbook observations jsonb key pair value';
|
||||
|
||||
CREATE TYPE public_type AS ENUM ('public_logs', 'public_logs_list', 'public_timelapse', 'public_monitoring', 'public_stats');
|
||||
CREATE or replace FUNCTION api.ispublic_fn(IN boat TEXT, IN _type TEXT, IN _id INTEGER DEFAULT NULL) RETURNS BOOLEAN AS $ispublic$
|
||||
DECLARE
|
||||
vessel TEXT := '^' || boat || '$';
|
||||
anonymous BOOLEAN := False;
|
||||
valid_public_type BOOLEAN := False;
|
||||
public_logs BOOLEAN := False;
|
||||
BEGIN
|
||||
-- If boat is not NULL
|
||||
IF boat IS NULL THEN
|
||||
RAISE WARNING '-> ispublic_fn invalid input %', boat;
|
||||
RETURN False;
|
||||
END IF;
|
||||
-- Check if public_type is valid enum
|
||||
SELECT _type::name = any(enum_range(null::public_type)::name[]) INTO valid_public_type;
|
||||
IF valid_public_type IS False THEN
|
||||
-- Ignore entry if type is invalid
|
||||
RAISE WARNING '-> ispublic_fn invalid input type %', _type;
|
||||
RETURN False;
|
||||
END IF;
|
||||
|
||||
RAISE WARNING '-> ispublic_fn _type [%], _id [%]', _type, _id;
|
||||
IF _type ~ '^public_(logs|timelapse)$' AND _id > 0 THEN
|
||||
WITH log as (
|
||||
SELECT vessel_id from api.logbook l where l.id = _id
|
||||
)
|
||||
SELECT EXISTS (
|
||||
SELECT l.vessel_id
|
||||
FROM auth.accounts a, auth.vessels v, jsonb_each_text(a.preferences) as prefs, log l
|
||||
WHERE v.vessel_id = l.vessel_id
|
||||
AND a.email = v.owner_email
|
||||
AND a.preferences->>'public_vessel'::text ~* vessel
|
||||
AND prefs.key = _type::TEXT
|
||||
AND prefs.value::BOOLEAN = true
|
||||
) into anonymous;
|
||||
RAISE WARNING '-> ispublic_fn public_logs output boat:[%], type:[%], result:[%]', boat, _type, anonymous;
|
||||
IF anonymous IS True THEN
|
||||
RETURN True;
|
||||
END IF;
|
||||
ELSE
|
||||
SELECT EXISTS (
|
||||
SELECT a.email
|
||||
FROM auth.accounts a, jsonb_each_text(a.preferences) as prefs
|
||||
WHERE a.preferences->>'public_vessel'::text ~* vessel
|
||||
AND prefs.key = _type::TEXT
|
||||
AND prefs.value::BOOLEAN = true
|
||||
) into anonymous;
|
||||
RAISE WARNING '-> ispublic_fn output boat:[%], type:[%], result:[%]', boat, _type, anonymous;
|
||||
IF anonymous IS True THEN
|
||||
RETURN True;
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN False;
|
||||
END
|
||||
$ispublic$ language plpgsql security definer;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.ispublic_fn
|
||||
IS 'Is web page publicly accessible by register boat name and/or logbook id';
|
||||
|
@@ -12,8 +12,8 @@ DROP TABLE IF EXISTS auth.otp;
|
||||
CREATE TABLE IF NOT EXISTS auth.otp (
|
||||
-- update email type to CITEXT, https://www.postgresql.org/docs/current/citext.html
|
||||
user_email CITEXT NOT NULL PRIMARY KEY REFERENCES auth.accounts(email) ON DELETE RESTRICT,
|
||||
otp_pass VARCHAR(10) NOT NULL,
|
||||
otp_timestamp TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW(),
|
||||
otp_pass TEXT NOT NULL,
|
||||
otp_timestamp TIMESTAMPTZ DEFAULT NOW(),
|
||||
otp_tries SMALLINT NOT NULL DEFAULT '0'
|
||||
);
|
||||
-- Description
|
||||
@@ -22,7 +22,8 @@ COMMENT ON TABLE
|
||||
IS 'Stores temporal otp code for up to 15 minutes';
|
||||
-- Indexes
|
||||
CREATE INDEX otp_pass_idx ON auth.otp (otp_pass);
|
||||
CREATE INDEX otp_user_email_idx ON auth.otp (user_email);
|
||||
-- Duplicate Indexes
|
||||
--CREATE INDEX otp_user_email_idx ON auth.otp (user_email);
|
||||
|
||||
DROP FUNCTION IF EXISTS public.generate_uid_fn;
|
||||
CREATE OR REPLACE FUNCTION public.generate_uid_fn(size INT) RETURNS TEXT
|
||||
|
@@ -15,13 +15,13 @@ select current_database();
|
||||
--
|
||||
-- api_anonymous
|
||||
-- nologin
|
||||
-- api_anonymous role in the database with which to execute anonymous web requests, limit 10 connections
|
||||
-- api_anonymous role in the database with which to execute anonymous web requests, limit 20 connections
|
||||
-- api_anonymous allows JWT token generation with an expiration time via function api.login() from auth.accounts table
|
||||
create role api_anonymous WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOLOGIN NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 10;
|
||||
create role api_anonymous WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOLOGIN NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 20;
|
||||
comment on role api_anonymous is
|
||||
'The role that PostgREST will switch to when a user is not authenticated.';
|
||||
-- Limit to 10 connections
|
||||
--alter user api_anonymous connection limit 10;
|
||||
-- Limit to 20 connections
|
||||
--alter user api_anonymous connection limit 20;
|
||||
grant usage on schema api to api_anonymous;
|
||||
-- explicitly limit EXECUTE privileges to only signup and login and reset functions
|
||||
grant execute on function api.login(text,text) to api_anonymous;
|
||||
@@ -38,6 +38,20 @@ grant execute on function api.pushover_fn(text,text) to api_anonymous;
|
||||
grant execute on function api.telegram_fn(text,text) to api_anonymous;
|
||||
grant execute on function api.telegram_otp_fn(text) to api_anonymous;
|
||||
--grant execute on function api.generate_otp_fn(text) to api_anonymous;
|
||||
grant execute on function api.ispublic_fn(text,text,integer) to api_anonymous;
|
||||
grant execute on function api.timelapse_fn to api_anonymous;
|
||||
grant execute on function api.stats_logs_fn to api_anonymous;
|
||||
grant execute on function api.stats_stays_fn to api_anonymous;
|
||||
grant execute on function api.status_fn to api_anonymous;
|
||||
-- Allow read on TABLES on API schema
|
||||
--GRANT SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata,api.stays_at TO api_anonymous;
|
||||
-- Allow read on VIEWS on API schema
|
||||
--GRANT SELECT ON TABLE api.logs_view,api.moorages_view,api.stays_view TO api_anonymous;
|
||||
--GRANT SELECT ON TABLE api.log_view,api.moorage_view,api.stay_view,api.vessels_view TO api_anonymous;
|
||||
GRANT SELECT ON ALL TABLES IN SCHEMA api TO api_anonymous;
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO api_anonymous;
|
||||
--grant execute on function public.st_asgeojson(record,text,integer,boolean) to api_anonymous;
|
||||
--grant execute on function public.st_makepoint(float,float) to api_anonymous;
|
||||
|
||||
-- authenticator
|
||||
-- login role
|
||||
@@ -46,18 +60,19 @@ comment on role authenticator is
|
||||
'Role that serves as an entry-point for API servers such as PostgREST.';
|
||||
grant api_anonymous to authenticator;
|
||||
|
||||
-- Grafana user and role with login, read-only, limit 15 connections
|
||||
CREATE ROLE grafana WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 15 LOGIN PASSWORD 'mysecretpassword';
|
||||
-- Grafana user and role with login, read-only, limit 20 connections
|
||||
CREATE ROLE grafana WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 20 LOGIN PASSWORD 'mysecretpassword';
|
||||
comment on role grafana is
|
||||
'Role that grafana will use for authenticated web users.';
|
||||
-- Allow API schema and Tables
|
||||
GRANT USAGE ON SCHEMA api TO grafana;
|
||||
-- Allow read on SEQUENCE on API schema
|
||||
GRANT USAGE, SELECT ON SEQUENCE api.logbook_id_seq,api.metadata_id_seq,api.moorages_id_seq,api.stays_id_seq TO grafana;
|
||||
GRANT SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata TO grafana;
|
||||
-- Allow read on TABLES on API schema
|
||||
GRANT SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata,api.stays_at TO grafana;
|
||||
-- Allow read on VIEWS on API schema
|
||||
GRANT SELECT ON TABLE api.logs_view,api.moorages_view,api.stays_view TO grafana;
|
||||
GRANT SELECT ON TABLE api.log_view,api.moorage_view,api.stay_view,api.vessels_view TO grafana;
|
||||
GRANT SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata,api.stays_at TO grafana;
|
||||
GRANT SELECT ON TABLE api.monitoring_view,api.monitoring_view2,api.monitoring_view3 TO grafana;
|
||||
GRANT SELECT ON TABLE api.monitoring_humidity,api.monitoring_voltage,api.monitoring_temperatures TO grafana;
|
||||
-- Allow Auth schema and Tables
|
||||
@@ -65,8 +80,8 @@ GRANT USAGE ON SCHEMA auth TO grafana;
|
||||
GRANT SELECT ON TABLE auth.vessels TO grafana;
|
||||
GRANT EXECUTE ON FUNCTION public.citext_eq(citext, citext) TO grafana;
|
||||
|
||||
-- Grafana_auth authenticator user and role with login, read-only on auth.accounts, limit 15 connections
|
||||
CREATE ROLE grafana_auth WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 15 LOGIN PASSWORD 'mysecretpassword';
|
||||
-- Grafana_auth authenticator user and role with login, read-only on auth.accounts, limit 20 connections
|
||||
CREATE ROLE grafana_auth WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 20 LOGIN PASSWORD 'mysecretpassword';
|
||||
comment on role grafana_auth is
|
||||
'Role that grafana auth proxy authenticator via apache.';
|
||||
-- Allow read on VIEWS on API schema
|
||||
@@ -78,66 +93,45 @@ GRANT SELECT ON TABLE auth.accounts TO grafana_auth;
|
||||
GRANT SELECT ON TABLE auth.vessels TO grafana_auth;
|
||||
-- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO grafana_auth;
|
||||
GRANT EXECUTE ON FUNCTION public.citext_eq(citext, citext) TO grafana_auth;
|
||||
GRANT ALL ON SCHEMA public TO grafana_auth; -- Important if grafana database in pg
|
||||
|
||||
-- User:
|
||||
-- nologin, web api only
|
||||
-- read-only for all and Read-Write on logbook, stays and moorage except for specific (name, notes) COLUMNS
|
||||
-- read-only for all and Read on logbook, stays and moorage and Write only for specific (name, notes) COLUMNS
|
||||
CREATE ROLE user_role WITH NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION;
|
||||
comment on role user_role is
|
||||
'Role that PostgREST will switch to for authenticated web users.';
|
||||
GRANT user_role to authenticator;
|
||||
GRANT USAGE ON SCHEMA api TO user_role;
|
||||
-- Allow read on SEQUENCE on API schema
|
||||
GRANT USAGE, SELECT ON SEQUENCE api.logbook_id_seq,api.metadata_id_seq,api.moorages_id_seq,api.stays_id_seq TO user_role;
|
||||
-- Allow read on TABLES on API schema
|
||||
GRANT SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata,api.stays_at TO user_role;
|
||||
GRANT SELECT ON TABLE public.process_queue TO user_role;
|
||||
-- To check?
|
||||
GRANT SELECT ON TABLE auth.vessels TO user_role;
|
||||
-- Allow users to update certain columns
|
||||
GRANT UPDATE (name, notes) ON api.logbook TO user_role;
|
||||
GRANT UPDATE (name, notes, stay_code) ON api.stays TO user_role;
|
||||
-- Allow users to update certain columns on specific TABLES on API schema
|
||||
GRANT UPDATE (name, _from, _to, notes) ON api.logbook TO user_role;
|
||||
GRANT UPDATE (name, notes, stay_code, active, departed) ON api.stays TO user_role;
|
||||
GRANT UPDATE (name, notes, stay_code, home_flag) ON api.moorages TO user_role;
|
||||
-- Allow users to remove logs and stays
|
||||
GRANT DELETE ON api.logbook,api.stays,api.moorages TO user_role;
|
||||
-- Allow EXECUTE on all FUNCTIONS on API and public schema
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api TO user_role;
|
||||
-- explicitly limit EXECUTE privileges to pgrest db-pre-request function
|
||||
--GRANT EXECUTE ON FUNCTION public.check_jwt() TO user_role;
|
||||
-- Allow others functions or allow all in public !! ??
|
||||
--GRANT EXECUTE ON FUNCTION api.export_logbook_geojson_linestring_fn(int4) TO user_role;
|
||||
--GRANT EXECUTE ON FUNCTION public.st_asgeojson(text) TO user_role;
|
||||
--GRANT EXECUTE ON FUNCTION public.geography_eq(geography, geography) TO user_role;
|
||||
-- TODO should not be need !! ??
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO user_role;
|
||||
|
||||
-- pg15 feature security_invoker=true,security_barrier=true
|
||||
GRANT SELECT ON TABLE api.logs_view,api.moorages_view,api.stays_view TO user_role;
|
||||
GRANT SELECT ON TABLE api.log_view,api.moorage_view,api.stay_view,api.vessels_view TO user_role;
|
||||
GRANT SELECT ON TABLE api.monitoring_view,api.monitoring_view2,api.monitoring_view3 TO user_role;
|
||||
GRANT SELECT ON TABLE api.monitoring_view,api.monitoring_view2,api.monitoring_view3,api.explore_view TO user_role;
|
||||
GRANT SELECT ON TABLE api.monitoring_humidity,api.monitoring_voltage,api.monitoring_temperatures TO user_role;
|
||||
GRANT SELECT ON TABLE api.stats_moorages_away_view,api.versions_view TO user_role;
|
||||
GRANT SELECT ON TABLE api.total_info_view TO user_role;
|
||||
GRANT SELECT ON TABLE api.stats_logs_view TO user_role;
|
||||
GRANT SELECT ON TABLE api.stats_moorages_view TO user_role;
|
||||
GRANT SELECT ON TABLE api.eventlogs_view TO user_role;
|
||||
-- Update ownership for security user_role as run by web user.
|
||||
-- Web listing
|
||||
--ALTER VIEW api.stays_view OWNER TO user_role;
|
||||
--ALTER VIEW api.moorages_view OWNER TO user_role;
|
||||
--ALTER VIEW api.logs_view OWNER TO user_role;
|
||||
--ALTER VIEW api.vessel_p_view OWNER TO user_role;
|
||||
--ALTER VIEW api.monitoring_view OWNER TO user_role;
|
||||
-- Remove all permissions except select
|
||||
--REVOKE UPDATE, TRUNCATE, REFERENCES, DELETE, TRIGGER, INSERT ON TABLE api.stays_view FROM user_role;
|
||||
--REVOKE UPDATE, TRUNCATE, REFERENCES, DELETE, TRIGGER, INSERT ON TABLE api.moorages_view FROM user_role;
|
||||
--REVOKE UPDATE, TRUNCATE, REFERENCES, DELETE, TRIGGER, INSERT ON TABLE api.logs_view FROM user_role;
|
||||
--REVOKE UPDATE, TRUNCATE, REFERENCES, DELETE, TRIGGER, INSERT ON TABLE api.monitoring_view FROM user_role;
|
||||
|
||||
-- Allow read and update on VIEWS
|
||||
-- Web detail view
|
||||
--ALTER VIEW api.log_view OWNER TO user_role;
|
||||
-- Remove all permissions except select and update
|
||||
--REVOKE TRUNCATE, DELETE, TRIGGER, INSERT ON TABLE api.log_view FROM user_role;
|
||||
|
||||
ALTER VIEW api.vessels_view OWNER TO user_role;
|
||||
-- Remove all permissions except select and update
|
||||
REVOKE TRUNCATE, DELETE, TRIGGER, INSERT ON TABLE api.vessels_view FROM user_role;
|
||||
|
||||
GRANT SELECT ON TABLE api.vessels_view TO user_role;
|
||||
GRANT SELECT ON TABLE api.moorages_stays_view TO user_role;
|
||||
|
||||
-- Vessel:
|
||||
-- nologin
|
||||
@@ -147,8 +141,10 @@ comment on role vessel_role is
|
||||
'Role that PostgREST will switch to for authenticated web vessels.';
|
||||
GRANT vessel_role to authenticator;
|
||||
GRANT USAGE ON SCHEMA api TO vessel_role;
|
||||
GRANT INSERT, UPDATE, SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata TO vessel_role;
|
||||
-- Allow read on SEQUENCE on API schema
|
||||
GRANT USAGE, SELECT ON SEQUENCE api.logbook_id_seq,api.metadata_id_seq,api.moorages_id_seq,api.stays_id_seq TO vessel_role;
|
||||
-- Allow read/write on TABLES on API schema
|
||||
GRANT INSERT, UPDATE, SELECT ON TABLE api.metrics,api.logbook,api.moorages,api.stays,api.metadata TO vessel_role;
|
||||
GRANT INSERT ON TABLE public.process_queue TO vessel_role;
|
||||
GRANT USAGE, SELECT ON SEQUENCE public.process_queue_id_seq TO vessel_role;
|
||||
-- explicitly limit EXECUTE privileges to pgrest db-pre-request function
|
||||
@@ -158,7 +154,11 @@ GRANT EXECUTE ON FUNCTION public.trip_in_progress_fn(text) to vessel_role;
|
||||
GRANT EXECUTE ON FUNCTION public.stay_in_progress_fn(text) to vessel_role;
|
||||
-- hypertable get_partition_hash ?!?
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA _timescaledb_internal TO vessel_role;
|
||||
|
||||
-- on metrics st_makepoint
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO vessel_role;
|
||||
-- Oauth registration
|
||||
GRANT EXECUTE ON FUNCTION api.oauth() TO vessel_role;
|
||||
GRANT EXECUTE ON FUNCTION api.oauth_vessel(text,text) TO vessel_role;
|
||||
|
||||
--- Scheduler:
|
||||
-- TODO: currently cron function are run as super user, switch to scheduler role.
|
||||
@@ -230,6 +230,10 @@ CREATE POLICY api_scheduler_role ON api.metrics TO scheduler
|
||||
CREATE POLICY grafana_role ON api.metrics TO grafana
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
-- Allow anonymous to select based on the vessel.id
|
||||
CREATE POLICY api_anonymous_role ON api.metrics TO api_anonymous
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
|
||||
-- Be sure to enable row level security on the table
|
||||
ALTER TABLE api.logbook ENABLE ROW LEVEL SECURITY;
|
||||
@@ -254,6 +258,10 @@ CREATE POLICY api_scheduler_role ON api.logbook TO scheduler
|
||||
CREATE POLICY grafana_role ON api.logbook TO grafana
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
-- Allow anonymous to select based on the vessel.id
|
||||
CREATE POLICY api_anonymous_role ON api.logbook TO api_anonymous
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
|
||||
-- Be sure to enable row level security on the table
|
||||
ALTER TABLE api.stays ENABLE ROW LEVEL SECURITY;
|
||||
@@ -277,6 +285,10 @@ CREATE POLICY api_scheduler_role ON api.stays TO scheduler
|
||||
CREATE POLICY grafana_role ON api.stays TO grafana
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
-- Allow anonymous to select based on the vessel.id
|
||||
CREATE POLICY api_anonymous_role ON api.stays TO api_anonymous
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
|
||||
-- Be sure to enable row level security on the table
|
||||
ALTER TABLE api.moorages ENABLE ROW LEVEL SECURITY;
|
||||
@@ -300,6 +312,10 @@ CREATE POLICY api_scheduler_role ON api.moorages TO scheduler
|
||||
CREATE POLICY grafana_role ON api.moorages TO grafana
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
-- Allow anonymous to select based on the vessel.id
|
||||
CREATE POLICY api_anonymous_role ON api.moorages TO api_anonymous
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
|
||||
-- Be sure to enable row level security on the table
|
||||
ALTER TABLE auth.vessels ENABLE ROW LEVEL SECURITY;
|
||||
|
@@ -10,20 +10,24 @@ CREATE EXTENSION IF NOT EXISTS pg_cron; -- provides a simple cron-based job sche
|
||||
-- TRUNCATE table jobs
|
||||
--TRUNCATE TABLE cron.job CONTINUE IDENTITY RESTRICT;
|
||||
|
||||
-- Create a every 5 minutes or minute job cron_process_new_logbook_fn ??
|
||||
SELECT cron.schedule('cron_new_logbook', '*/5 * * * *', 'select public.cron_process_new_logbook_fn()');
|
||||
-- Create a every 5 minutes or minute job cron_process_pre_logbook_fn ??
|
||||
SELECT cron.schedule('cron_pre_logbook', '*/5 * * * *', 'select public.cron_process_pre_logbook_fn()');
|
||||
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_pre_logbook';
|
||||
|
||||
-- Create a every 6 minutes or minute job cron_process_new_logbook_fn ??
|
||||
SELECT cron.schedule('cron_new_logbook', '*/6 * * * *', 'select public.cron_process_new_logbook_fn()');
|
||||
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_logbook';
|
||||
|
||||
-- Create a every 5 minute job cron_process_new_stay_fn
|
||||
SELECT cron.schedule('cron_new_stay', '*/6 * * * *', 'select public.cron_process_new_stay_fn()');
|
||||
-- Create a every 7 minute job cron_process_new_stay_fn
|
||||
SELECT cron.schedule('cron_new_stay', '*/7 * * * *', 'select public.cron_process_new_stay_fn()');
|
||||
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_stay';
|
||||
|
||||
-- Create a every 6 minute job cron_process_new_moorage_fn, delay from stay to give time to generate geo reverse location, eg: name
|
||||
SELECT cron.schedule('cron_new_moorage', '*/7 * * * *', 'select public.cron_process_new_moorage_fn()');
|
||||
--SELECT cron.schedule('cron_new_moorage', '*/7 * * * *', 'select public.cron_process_new_moorage_fn()');
|
||||
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_moorage';
|
||||
|
||||
-- Create a every 10 minute job cron_process_monitor_offline_fn
|
||||
SELECT cron.schedule('cron_monitor_offline', '*/10 * * * *', 'select public.cron_process_monitor_offline_fn()');
|
||||
-- Create a every 11 minute job cron_process_monitor_offline_fn
|
||||
SELECT cron.schedule('cron_monitor_offline', '*/11 * * * *', 'select public.cron_process_monitor_offline_fn()');
|
||||
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_monitor_offline';
|
||||
|
||||
-- Create a every 10 minute job cron_process_monitor_online_fn
|
||||
@@ -42,18 +46,25 @@ SELECT cron.schedule('cron_monitor_online', '*/10 * * * *', 'select public.cron_
|
||||
--SELECT cron.schedule('cron_new_account_otp', '*/6 * * * *', 'select public.cron_process_new_account_otp_validation_fn()');
|
||||
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_account_otp';
|
||||
|
||||
-- Create a every 5 minute job cron_process_grafana_fn
|
||||
SELECT cron.schedule('cron_grafana', '*/5 * * * *', 'select public.cron_process_grafana_fn()');
|
||||
|
||||
-- Notification
|
||||
-- Create a every 1 minute job cron_process_new_notification_queue_fn, new_account, new_vessel, _new_account_otp
|
||||
SELECT cron.schedule('cron_new_notification', '*/2 * * * *', 'select public.cron_process_new_notification_fn()');
|
||||
SELECT cron.schedule('cron_new_notification', '*/1 * * * *', 'select public.cron_process_new_notification_fn()');
|
||||
--UPDATE cron.job SET database = 'signalk' where jobname = 'cron_new_notification';
|
||||
|
||||
-- Maintenance
|
||||
-- Vacuum database at “At 01:01 on Sunday.”
|
||||
SELECT cron.schedule('cron_vacuum', '1 1 * * 0', 'VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) api.logbook,api.stays,api.moorages,api.metadata,api.metrics;');
|
||||
-- Remove all jobs log at “At 02:02 on Sunday.”
|
||||
-- Vacuum database schema api at "At 01:31 on Sunday."
|
||||
SELECT cron.schedule('cron_vacuum_api', '31 1 * * 0', 'VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) api.logbook,api.stays,api.moorages,api.metadata,api.metrics;');
|
||||
-- Vacuum database schema auth at "At 01:01 on Sunday."
|
||||
SELECT cron.schedule('cron_vacuum_auth', '1 1 * * 0', 'VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) auth.accounts,auth.vessels,auth.otp;');
|
||||
-- Remove old jobs log at "At 02:02 on Sunday."
|
||||
SELECT cron.schedule('job_run_details_cleanup', '2 2 * * 0', 'select public.job_run_details_cleanup_fn()');
|
||||
-- Rebuilding indexes at “first day of each month at 23:01.”
|
||||
SELECT cron.schedule('cron_reindex', '1 23 1 * *', 'REINDEX TABLE api.logbook; REINDEX TABLE api.stays; REINDEX TABLE api.moorages; REINDEX TABLE api.metadata; REINDEX TABLE api.metrics;');
|
||||
-- Rebuilding indexes schema api at "first day of each month at 23:15."
|
||||
SELECT cron.schedule('cron_reindex_api', '15 23 1 * *', 'REINDEX TABLE CONCURRENTLY api.logbook; REINDEX TABLE CONCURRENTLY api.stays; REINDEX TABLE CONCURRENTLY api.moorages; REINDEX TABLE CONCURRENTLY api.metadata;');
|
||||
-- Rebuilding indexes schema auth at "first day of each month at 23:01."
|
||||
SELECT cron.schedule('cron_reindex_auth', '1 23 1 * *', 'REINDEX TABLE CONCURRENTLY auth.accounts; REINDEX TABLE CONCURRENTLY auth.vessels; REINDEX TABLE CONCURRENTLY auth.otp;');
|
||||
-- Any other maintenance require?
|
||||
|
||||
-- OTP
|
||||
@@ -66,15 +77,17 @@ SELECT cron.schedule('cron_prune_otp', '*/15 * * * *', 'select public.cron_proce
|
||||
|
||||
-- Notifications/Reminders of no vessel & no metadata & no activity
|
||||
-- At 08:05 on Sunday.
|
||||
SELECT cron.schedule('cron_no_vessel', '05 08 * * 0', 'select public.cron_process_no_vessel_fn()');
|
||||
SELECT cron.schedule('cron_no_metadata', '05 08 * * 0', 'select public.cron_process_no_metadata_fn()');
|
||||
SELECT cron.schedule('cron_no_activity', '05 08 * * 0', 'select public.cron_process_no_activity_fn()');
|
||||
-- At 08:05 on every 4th day-of-month if it's on Sunday.
|
||||
SELECT cron.schedule('cron_no_vessel', '5 8 */4 * 0', 'select public.cron_process_no_vessel_fn()');
|
||||
SELECT cron.schedule('cron_no_metadata', '5 8 */4 * 0', 'select public.cron_process_no_metadata_fn()');
|
||||
SELECT cron.schedule('cron_no_activity', '5 8 */4 * 0', 'select public.cron_process_no_activity_fn()');
|
||||
|
||||
-- Cron job settings
|
||||
UPDATE cron.job SET database = 'signalk';
|
||||
UPDATE cron.job SET username = 'username'; -- TODO update to scheduler, pending process_queue update
|
||||
--UPDATE cron.job SET username = 'username' where jobname = 'cron_vacuum'; -- TODO Update to superuser for vaccuum permissions
|
||||
--UPDATE cron.job SET username = 'username' where jobname = 'cron_vacuum'; -- TODO Update to superuser for vacuum permissions
|
||||
UPDATE cron.job SET nodename = '/var/run/postgresql/'; -- VS default localhost ??
|
||||
UPDATE cron.job SET database = 'postgres' WHERE jobname = 'job_run_details_cleanup';
|
||||
-- check job lists
|
||||
SELECT * FROM cron.job;
|
||||
-- unschedule by job id
|
||||
|
@@ -18,7 +18,7 @@ select current_database();
|
||||
\c signalk
|
||||
|
||||
CREATE TABLE public.ne_10m_geography_marine_polys (
|
||||
gid serial4 NOT NULL,
|
||||
gid INT GENERATED ALWAYS AS IDENTITY NOT NULL,
|
||||
featurecla TEXT NULL,
|
||||
"name" TEXT NULL,
|
||||
namealt TEXT NULL,
|
||||
|
@@ -17,6 +17,8 @@ INSERT INTO app_settings (name, value) VALUES
|
||||
('app.pushover_app_token', '${PGSAIL_PUSHOVER_APP_TOKEN}'),
|
||||
('app.pushover_app_url', '${PGSAIL_PUSHOVER_APP_URL}'),
|
||||
('app.telegram_bot_token', '${PGSAIL_TELEGRAM_BOT_TOKEN}'),
|
||||
('app.grafana_admin_uri', '${PGSAIL_GRAFANA_ADMIN_URI}'),
|
||||
('app.keycloak_uri', '${PGSAIL_KEYCLOAK_URI}'),
|
||||
('app.url', '${PGSAIL_APP_URL}'),
|
||||
('app.version', '${PGSAIL_VERSION}');
|
||||
-- Update comment with version
|
||||
|
@@ -1 +1 @@
|
||||
0.3.0
|
||||
0.6.1
|
||||
|
@@ -59,7 +59,7 @@ 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: 2},
|
||||
{ url: '/logs_view', res_body_length: 0},
|
||||
{ url: '/log_view', res_body_length: 2},
|
||||
//{ url: '/stats_view', res_body_length: 1},
|
||||
{ url: '/vessels_view', res_body_length: 1},
|
||||
@@ -198,7 +198,7 @@ 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: 1},
|
||||
{ url: '/logs_view', res_body_length: 0},
|
||||
{ url: '/log_view', res_body_length: 1},
|
||||
//{ url: '/stats_view', res_body_length: 1},
|
||||
{ url: '/vessels_view', res_body_length: 1},
|
||||
@@ -604,8 +604,12 @@ request.set('User-Agent', 'PostgSail unit tests');
|
||||
// Override client_id
|
||||
data[i]['client_id'] = test.vessel_metadata.client_id;
|
||||
}
|
||||
// Force last entry to be back in time from previous, it should be ignore silently
|
||||
data.at(-1).time = moment.utc(data.at(-2).time).subtract(1, 'minutes').format();
|
||||
// The last entry are invalid and should be ignore.
|
||||
// - Invalid status
|
||||
// - Invalid speedoverground
|
||||
// - Invalid time previous time is duplicate
|
||||
// Force last valid entry to be back in time from previous, it should be ignore silently
|
||||
data.at(-1).time = moment.utc(data.at(-3).time).subtract(1, 'minutes').format();
|
||||
//console.log(data[0]);
|
||||
|
||||
it('/metrics?select=time', function(done) {
|
||||
@@ -625,7 +629,7 @@ request.set('User-Agent', 'PostgSail unit tests');
|
||||
res.header['content-type'].should.match(new RegExp('json','g'));
|
||||
res.header['server'].should.match(new RegExp('postgrest','g'));
|
||||
should.exist(res.body);
|
||||
res.body.length.should.match(test.vessel_metrics['metrics'].length-1);
|
||||
res.body.length.should.match(test.vessel_metrics['metrics'].length-3);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
@@ -47,7 +47,7 @@ const metrics_simulator = require('./metrics_sample_simulator.json');
|
||||
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: 2},
|
||||
{ url: '/logs_view', res_body_length: 1},
|
||||
{ url: '/log_view', res_body_length: 2},
|
||||
//{ url: '/stats_view', res_body_length: 1},
|
||||
{ url: '/vessels_view', res_body_length: 1},
|
||||
@@ -211,7 +211,7 @@ request.set('User-Agent', 'PostgSail unit tests');
|
||||
res.header['content-type'].should.match(new RegExp('json','g'));
|
||||
res.header['server'].should.match(new RegExp('postgrest','g'));
|
||||
should.exist(res.body.token);
|
||||
res.body.token.should.match(user_jwt);
|
||||
//res.body.token.should.match(user_jwt);
|
||||
console.log(user_jwt);
|
||||
should.exist(user_jwt);
|
||||
done(err);
|
||||
|
@@ -43,7 +43,7 @@ var moment = require('moment');
|
||||
},
|
||||
user_tables: [
|
||||
{ url: '/stays', res_body_length: 3},
|
||||
{ url: '/moorages', res_body_length: 2},
|
||||
{ url: '/moorages', res_body_length: 3},
|
||||
{ url: '/logbook', res_body_length: 2},
|
||||
{ url: '/metadata', res_body_length: 1}
|
||||
],
|
||||
@@ -103,6 +103,14 @@ var moment = require('moment');
|
||||
obj_name: null
|
||||
}
|
||||
},
|
||||
{ url: '/rpc/export_logbook_kml_fn',
|
||||
payload: {
|
||||
_id: 2
|
||||
},
|
||||
res: {
|
||||
obj_name: null
|
||||
}
|
||||
},
|
||||
{ url: '/rpc/export_moorages_geojson_fn',
|
||||
payload: {},
|
||||
res: {
|
||||
@@ -233,7 +241,7 @@ var moment = require('moment');
|
||||
},
|
||||
user_tables: [
|
||||
{ url: '/stays', res_body_length: 3},
|
||||
{ url: '/moorages', res_body_length: 2},
|
||||
{ url: '/moorages', res_body_length: 4},
|
||||
{ url: '/logbook', res_body_length: 2},
|
||||
{ url: '/metadata', res_body_length: 1}
|
||||
],
|
||||
@@ -293,6 +301,32 @@ var moment = require('moment');
|
||||
obj_name: null
|
||||
}
|
||||
},
|
||||
{ url: '/rpc/export_logbook_kml_fn',
|
||||
payload: {
|
||||
_id: 4
|
||||
},
|
||||
res: {
|
||||
obj_name: null
|
||||
}
|
||||
},
|
||||
{ url: '/rpc/export_logbooks_gpx_fn',
|
||||
payload: {
|
||||
start_log: 3,
|
||||
end_log: 4
|
||||
},
|
||||
res: {
|
||||
obj_name: null
|
||||
}
|
||||
},
|
||||
{ url: '/rpc/export_logbooks_kml_fn',
|
||||
payload: {
|
||||
start_log: 3,
|
||||
end_log: 4
|
||||
},
|
||||
res: {
|
||||
obj_name: null
|
||||
}
|
||||
},
|
||||
{ url: '/rpc/export_moorages_geojson_fn',
|
||||
payload: {},
|
||||
res: {
|
||||
@@ -650,7 +684,7 @@ request.set('User-Agent', 'PostgSail unit tests');
|
||||
.set('Authorization', `Bearer ${user_jwt}`)
|
||||
.set('Accept', 'application/json')
|
||||
.end(function(err,res){
|
||||
//console.log(res.body);
|
||||
console.log(res.body);
|
||||
res.status.should.equal(200);
|
||||
should.exist(res.header['content-type']);
|
||||
should.exist(res.header['server']);
|
||||
|
196
tests/index4.js
@@ -3,7 +3,7 @@
|
||||
* Unit test #4
|
||||
* OTP for email, Pushover, Telegram
|
||||
*
|
||||
* process.env.PGSAIL_API_URI = from inside the docker
|
||||
* process.env.PGSAIL_API_URI = from inside the docker
|
||||
*
|
||||
* npm install supertest should mocha mochawesome moment
|
||||
* alias mocha="./node_modules/mocha/bin/_mocha"
|
||||
@@ -154,6 +154,16 @@ var moment = require("moment");
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
public: [
|
||||
{
|
||||
url: "/rpc/update_user_preferences_fn",
|
||||
payload: { key: "{public_logs}", value: true },
|
||||
},
|
||||
{
|
||||
url: "/rpc/update_user_preferences_fn",
|
||||
payload: { key: "{public_monitoring}", value: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
cname: process.env.PGSAIL_API_URI,
|
||||
@@ -285,6 +295,16 @@ var moment = require("moment");
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
public: [
|
||||
{
|
||||
url: "/rpc/update_user_preferences_fn",
|
||||
payload: { key: "{public_logs}", value: true },
|
||||
},
|
||||
{
|
||||
url: "/rpc/update_user_preferences_fn",
|
||||
payload: { key: "{public_monitoring}", value: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
].forEach(function (test) {
|
||||
//console.log(`${test.cname}`);
|
||||
@@ -581,91 +601,127 @@ var moment = require("moment");
|
||||
}); // Function endpoint
|
||||
*/
|
||||
|
||||
describe("Badges, user jwt", function () {
|
||||
it("/rpc/settings_fn return user settings", function (done) {
|
||||
// Reset agent so we do not save cookies
|
||||
request = supertest.agent(test.cname);
|
||||
request
|
||||
.post("/rpc/settings_fn")
|
||||
.set("Authorization", `Bearer ${user_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.settings);
|
||||
should.exist(res.body.settings.preferences.badges)
|
||||
let badges = res.body.settings.preferences.badges;
|
||||
//console.log(Object.keys(badges));
|
||||
Object.keys(badges).length.should.be.aboveOrEqual(3);
|
||||
(badges).should.have.properties('Helmsman', 'Wake Maker', 'Stormtrooper');
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
}); // user JWT
|
||||
|
||||
describe("Function monitoring endpoint, JWT user_role", function () {
|
||||
let otp = null;
|
||||
test.monitoring.forEach(function (subtest) {
|
||||
it(`${subtest.url}`, function (done) {
|
||||
try {
|
||||
describe("Badges, user jwt", function () {
|
||||
it("/rpc/settings_fn return user settings", function (done) {
|
||||
// Reset agent so we do not save cookies
|
||||
request = supertest.agent(test.cname);
|
||||
request
|
||||
.get(subtest.url)
|
||||
.post("/rpc/settings_fn")
|
||||
.set("Authorization", `Bearer ${user_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["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.settings);
|
||||
should.exist(res.body.settings.preferences.badges);
|
||||
let badges = res.body.settings.preferences.badges;
|
||||
//console.log(Object.keys(badges));
|
||||
Object.keys(badges).length.should.be.aboveOrEqual(3);
|
||||
badges.should.have.properties(
|
||||
"Helmsman",
|
||||
"Wake Maker",
|
||||
"Stormtrooper"
|
||||
);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
}); // user JWT
|
||||
|
||||
describe("Function monitoring endpoint, JWT user_role", function () {
|
||||
let otp = null;
|
||||
test.monitoring.forEach(function (subtest) {
|
||||
it(`${subtest.url}`, function (done) {
|
||||
try {
|
||||
// Reset agent so we do not save cookies
|
||||
request = supertest.agent(test.cname);
|
||||
request
|
||||
.get(subtest.url)
|
||||
.set("Authorization", `Bearer ${user_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);
|
||||
//let monitoring = res.body;
|
||||
//console.log(monitoring);
|
||||
// minimum set for static monitoring page
|
||||
// no value for humidity monitoring
|
||||
//monitoring.length.should.be.aboveOrEqual(21);
|
||||
done(err);
|
||||
});
|
||||
} catch (error) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
}); // Monitoring endpoint
|
||||
|
||||
describe("Event Logs, user jwt", function () {
|
||||
it("/eventlogs_view endpoint, list process_queue, JWT user_role", function (done) {
|
||||
// Reset agent so we do not save cookies
|
||||
request = supertest.agent(test.cname);
|
||||
request
|
||||
.get("/eventlogs_view")
|
||||
.set("Authorization", `Bearer ${user_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);
|
||||
//let monitoring = res.body;
|
||||
//console.log(monitoring);
|
||||
// minimum set for static monitoring page
|
||||
// no value for humidity monitoring
|
||||
//monitoring.length.should.be.aboveOrEqual(21);
|
||||
let event = res.body;
|
||||
//console.log(event);
|
||||
// minimum events log for kapla & aava 13 + 4 email_otp = 17
|
||||
event.length.should.be.aboveOrEqual(13);
|
||||
done(err);
|
||||
});
|
||||
} catch (error) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
}); // Monitoring endpoint
|
||||
|
||||
describe("Event Logs, user jwt", function () {
|
||||
it("/eventlogs_view endpoint, list process_queue, JWT user_role", function (done) {
|
||||
// Reset agent so we do not save cookies
|
||||
request = supertest.agent(test.cname);
|
||||
request
|
||||
.get("/eventlogs_view")
|
||||
.set("Authorization", `Bearer ${user_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);
|
||||
let event = res.body;
|
||||
//console.log(event);
|
||||
// minimum events log for kapla & aava 13 + 4 email_otp = 17
|
||||
event.length.should.be.aboveOrEqual(13);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
}); // user JWT
|
||||
}); // user JWT
|
||||
|
||||
describe("Function update preference for public access endpoint, JWT user_role", function () {
|
||||
test.public.forEach(function (subtest) {
|
||||
it(`${subtest.url}`, function (done) {
|
||||
try {
|
||||
// Reset agent so we do not save cookies
|
||||
request = supertest.agent(test.cname);
|
||||
request
|
||||
.post(subtest.url)
|
||||
.send(subtest.payload)
|
||||
.set("Authorization", `Bearer ${user_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["server"].should.match(new RegExp("postgrest", "g"));
|
||||
//console.log(res.body);
|
||||
should.exist(res.body);
|
||||
//let monitoring = res.body;
|
||||
//console.log(monitoring);
|
||||
// minimum set for static monitoring page
|
||||
// no value for humidity monitoring
|
||||
//monitoring.length.should.be.aboveOrEqual(21);
|
||||
done(err);
|
||||
});
|
||||
} catch (error) {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
}); // Monitoring endpoint
|
||||
|
||||
}); // OpenAPI description
|
||||
}); // Users Array
|
||||
|
223
tests/index5.js
Normal file
@@ -0,0 +1,223 @@
|
||||
"use strict";
|
||||
/*
|
||||
* Unit test #5
|
||||
* Public/Anonymous access
|
||||
*
|
||||
* process.env.PGSAIL_API_URI = from inside the docker
|
||||
*
|
||||
* npm install supertest should mocha mochawesome moment
|
||||
* alias mocha="./node_modules/mocha/bin/_mocha"
|
||||
* mocha index5.js --reporter mochawesome --reporter-options reportDir=/mnt/postgsail/,reportFilename=report_api.html
|
||||
*
|
||||
*/
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const supertest = require("supertest");
|
||||
// Deprecated
|
||||
const should = require("should");
|
||||
//const chai = require("chai");
|
||||
//const should = chai.should();
|
||||
let request = null;
|
||||
var moment = require("moment");
|
||||
|
||||
// Users Array
|
||||
[
|
||||
{
|
||||
cname: process.env.PGSAIL_API_URI,
|
||||
name: "PostgSail unit test kapla",
|
||||
logs: {
|
||||
url: "/logs_view",
|
||||
header: { name: "x-is-public", value: btoa("kapla,public_logs_list,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
log: {
|
||||
url: "/log_view?id=eq.1",
|
||||
header: { name: "x-is-public", value: btoa("kapla,public_logs,1") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
monitoring: {
|
||||
url: "/monitoring_view",
|
||||
header: { name: "x-is-public", value: btoa("kapla,public_monitoring,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
timelapse: {
|
||||
url: "/rpc/timelapse_fn",
|
||||
header: { name: "x-is-public", value: btoa("kapla,public_timelapse,1") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
timelapse_full: {
|
||||
url: "/rpc/timelapse_fn",
|
||||
header: { name: "x-is-public", value: btoa("kapla,public_timelapse,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
stats_logs: {
|
||||
url: "/rpc/stats_logs_fn",
|
||||
header: { name: "x-is-public", value: btoa("kapla,public_stats,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
stats_stays: {
|
||||
url: "/rpc/stats_stay_fn",
|
||||
header: { name: "x-is-public", value: btoa("kapla,public_stats,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
export_gpx: {
|
||||
url: "/rpc/export_logbook_gpx_fn",
|
||||
header: { name: "x-is-public", value: btoa("kapla,public_logs,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
cname: process.env.PGSAIL_API_URI,
|
||||
name: "PostgSail unit test, aava",
|
||||
logs: {
|
||||
url: "/logs_view",
|
||||
header: { name: "x-is-public", value: btoa("aava,public_logs_list,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
log: {
|
||||
url: "/log_view?id=eq.3",
|
||||
header: { name: "x-is-public", value: btoa("aava,public_logs,3") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
monitoring: {
|
||||
url: "/monitoring_view",
|
||||
header: { name: "x-is-public", value: btoa("aava,public_monitoring,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
timelapse: {
|
||||
url: "/rpc/timelapse_fn",
|
||||
header: { name: "x-is-public", value: btoa("aava,public_timelapse,3") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
timelapse_full: {
|
||||
url: "/rpc/timelapse_fn",
|
||||
header: { name: "x-is-public", value: btoa("aava,public_timelapse,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
stats_logs: {
|
||||
url: "/rpc/stats_logs_fn",
|
||||
header: { name: "x-is-public", value: btoa("aava,public_stats,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
stats_stays: {
|
||||
url: "/rpc/stats_stay_fn",
|
||||
header: { name: "x-is-public", value: btoa("kapla,public_stats,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
export_gpx: {
|
||||
url: "/rpc/export_logbook_gpx_fn",
|
||||
header: { name: "x-is-public", value: btoa("aava,public_logs,0") },
|
||||
payload: null,
|
||||
res: {},
|
||||
},
|
||||
},
|
||||
].forEach(function (test) {
|
||||
//console.log(`${test.cname}`);
|
||||
describe(`${test.name}`, function () {
|
||||
request = supertest.agent(test.cname);
|
||||
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) {
|
||||
// Reset agent so we do not save cookies
|
||||
request = supertest.agent(test.cname);
|
||||
request
|
||||
.get(test.logs.url)
|
||||
.set(test.logs.header.name, test.logs.header.value)
|
||||
.set("Accept", "application/json")
|
||||
.end(function (err, res) {
|
||||
res.status.should.equal(404);
|
||||
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"));
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
it("/log_view, api_anonymous no jwt token", function (done) {
|
||||
// Reset agent so we do not save cookies
|
||||
request = supertest.agent(test.cname);
|
||||
request
|
||||
.get(test.log.url)
|
||||
.set(test.log.header.name, test.log.header.value)
|
||||
.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"));
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
it("/monitoring_view, api_anonymous no jwt token", function (done) {
|
||||
// Reset agent so we do not save cookies
|
||||
request = supertest.agent(test.cname);
|
||||
request
|
||||
.get(test.monitoring.url)
|
||||
.set(test.monitoring.header.name, test.monitoring.header.value)
|
||||
.set("Accept", "application/json")
|
||||
.end(function (err, res) {
|
||||
console.log(res.text);
|
||||
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"));
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
it("/rpc/timelapse_fn, api_anonymous no jwt token", function (done) {
|
||||
// Reset agent so we do not save cookies
|
||||
request = supertest.agent(test.cname);
|
||||
request
|
||||
.post(test.timelapse.url)
|
||||
.set(test.timelapse.header.name, test.timelapse.header.value)
|
||||
.set("Accept", "application/json")
|
||||
.end(function (err, res) {
|
||||
console.log(res.text);
|
||||
res.status.should.equal(404); // return 404 as it is not enable in user settings.
|
||||
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"));
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
it("/rpc/export_logbook_gpx_fn, api_anonymous no jwt token", function (done) {
|
||||
// Reset agent so we do not save cookies
|
||||
request = supertest.agent(test.cname);
|
||||
request
|
||||
.post(test.export_gpx.url)
|
||||
.send({_id: 1})
|
||||
.set(test.export_gpx.header.name, test.export_gpx.header.value)
|
||||
.set("Accept", "application/json")
|
||||
.end(function (err, res) {
|
||||
console.log(res.text)
|
||||
res.status.should.equal(401);
|
||||
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"));
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
}); // user JWT
|
||||
}); // OpenAPI description
|
||||
}); // Users Array
|
@@ -21,7 +21,19 @@
|
||||
"courseovergroundtrue" : 197.4,
|
||||
"windspeedapparent" : 15.4,
|
||||
"anglespeedapparent" : 43.0,
|
||||
"status" : "moored",
|
||||
"status" : "sailing",
|
||||
"metrics" : {"navigation.log": 17441395, "navigation.trip.log": 80284, "navigation.headingTrue": 3.4924, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 32.289, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 231, "electrical.batteries.1.voltage": 14.45, "navigation.gnss.antennaAltitude": -0.04, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 32.29, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 57, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 4, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 11, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
|
||||
},
|
||||
{
|
||||
"time" : "2022-07-31T11:29:13.340Z",
|
||||
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
|
||||
"latitude" : 59.7213961,
|
||||
"longitude" : 24.7349507,
|
||||
"speedoverground" : 6.5,
|
||||
"courseovergroundtrue" : 197.4,
|
||||
"windspeedapparent" : 15.4,
|
||||
"anglespeedapparent" : 43.0,
|
||||
"status" : "sailing",
|
||||
"metrics" : {"navigation.log": 17441395, "navigation.trip.log": 80284, "navigation.headingTrue": 3.4924, "navigation.gnss.satellites": 11, "environment.depth.belowKeel": 32.289, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.34, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 231, "electrical.batteries.1.voltage": 14.45, "navigation.gnss.antennaAltitude": -0.04, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 32.29, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 57, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 4, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 11, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
|
||||
},
|
||||
{
|
||||
@@ -561,7 +573,31 @@
|
||||
"courseovergroundtrue" : 197.6,
|
||||
"windspeedapparent" : 15.9,
|
||||
"anglespeedapparent" : 31.0,
|
||||
"status" : "ais-sart",
|
||||
"metrics" : {}
|
||||
},
|
||||
{
|
||||
"time" : "2022-07-31T12:14:29.168Z",
|
||||
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
|
||||
"latitude" : 59.7124174,
|
||||
"longitude" : 24.7289112,
|
||||
"speedoverground" : 55.7,
|
||||
"courseovergroundtrue" : 197.6,
|
||||
"windspeedapparent" : 15.9,
|
||||
"anglespeedapparent" : 31.0,
|
||||
"status" : "anchored",
|
||||
"metrics" : {"navigation.log": 17442506, "navigation.trip.log": 81321, "navigation.headingTrue": 3.571, "navigation.gnss.satellites": 10, "environment.depth.belowKeel": 13.749, "navigation.magneticVariation": 0.1414, "navigation.speedThroughWater": 3.07, "environment.water.temperature": 313.15, "electrical.batteries.1.current": 43.9, "electrical.batteries.1.voltage": 14.54, "navigation.gnss.antennaAltitude": 2.05, "network.n2k.ngt-1.130356.errorID": 0, "network.n2k.ngt-1.130356.modelID": 14, "environment.depth.belowTransducer": 13.75, "electrical.batteries.1.temperature": 299.82, "environment.depth.transducerToKeel": -0.001, "navigation.gnss.horizontalDilution": 0.8, "network.n2k.ngt-1.130356.ch1.rxLoad": 4, "network.n2k.ngt-1.130356.ch1.txLoad": 0, "network.n2k.ngt-1.130356.ch2.rxLoad": 0, "network.n2k.ngt-1.130356.ch2.txLoad": 40, "network.n2k.ngt-1.130356.ch1.deleted": 0, "network.n2k.ngt-1.130356.ch2.deleted": 0, "network.n2k.ngt-1.130356.ch2Bandwidth": 4, "network.n2k.ngt-1.130356.ch1.bandwidth": 3, "network.n2k.ngt-1.130356.ch1.rxDropped": 0, "network.n2k.ngt-1.130356.ch2.rxDropped": 0, "network.n2k.ngt-1.130356.ch1.rxFiltered": 0, "network.n2k.ngt-1.130356.ch2.rxFiltered": 0, "network.n2k.ngt-1.130356.ch1.rxBandwidth": 5, "network.n2k.ngt-1.130356.ch1.txBandwidth": 0, "network.n2k.ngt-1.130356.ch2.rxBandwidth": 0, "network.n2k.ngt-1.130356.ch2.txBandwidth": 10, "network.n2k.ngt-1.130356.uniChannelCount": 2, "network.n2k.ngt-1.130356.indiChannelCount": 2, "network.n2k.ngt-1.130356.ch1.BufferLoading": 0, "network.n2k.ngt-1.130356.ch2.bufferLoading": 0, "network.n2k.ngt-1.130356.ch1.PointerLoading": 0, "network.n2k.ngt-1.130356.ch2.pointerLoading": 0}
|
||||
"metrics" : {}
|
||||
},
|
||||
{
|
||||
"time" : "2022-07-31T12:14:29.168Z",
|
||||
"client_id" : "vessels.urn:mrn:imo:mmsi:987654321",
|
||||
"latitude" : 59.7124174,
|
||||
"longitude" : 24.7289112,
|
||||
"speedoverground" : 5.7,
|
||||
"courseovergroundtrue" : 197.6,
|
||||
"windspeedapparent" : 15.9,
|
||||
"anglespeedapparent" : 31.0,
|
||||
"status" : "anchored",
|
||||
"metrics" : {}
|
||||
}
|
||||
]}
|
||||
|
@@ -12,6 +12,18 @@
|
||||
"status" : "moored",
|
||||
"metrics" : {"environment.wind.speedTrue": 4.44, "navigation.speedThroughWater": 3.0918118943701245, "performance.velocityMadeGood": 2.9323340761912995, "environment.wind.angleTrueWater": -0.3665191430024964, "environment.depth.belowTransducer": 13.1, "navigation.courseOverGroundMagnetic": 3.620685534088946, "navigation.courseRhumbline.crossTrackError": 0}
|
||||
},
|
||||
{
|
||||
"time" : "2022-07-30T14:52:28.000Z",
|
||||
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
|
||||
"latitude" : 60.077666666666666,
|
||||
"longitude" : 23.530866666666668,
|
||||
"speedoverground" : 0.0,
|
||||
"courseovergroundtrue" : 207.5,
|
||||
"windspeedapparent" : 14.8,
|
||||
"anglespeedapparent" : -12.0,
|
||||
"status" : "sailing",
|
||||
"metrics" : {"environment.wind.speedTrue": 4.44, "navigation.speedThroughWater": 3.0918118943701245, "performance.velocityMadeGood": 2.9323340761912995, "environment.wind.angleTrueWater": -0.3665191430024964, "environment.depth.belowTransducer": 13.1, "navigation.courseOverGroundMagnetic": 3.620685534088946, "navigation.courseRhumbline.crossTrackError": 0, "propulsion.main.runTime":1776241 }
|
||||
},
|
||||
{
|
||||
"time" : "2022-07-30T14:53:28.000Z",
|
||||
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
|
||||
@@ -21,8 +33,8 @@
|
||||
"courseovergroundtrue" : 207.5,
|
||||
"windspeedapparent" : 14.8,
|
||||
"anglespeedapparent" : -12.0,
|
||||
"status" : "moored",
|
||||
"metrics" : {"environment.wind.speedTrue": 4.44, "navigation.speedThroughWater": 3.0918118943701245, "performance.velocityMadeGood": 2.9323340761912995, "environment.wind.angleTrueWater": -0.3665191430024964, "environment.depth.belowTransducer": 13.1, "navigation.courseOverGroundMagnetic": 3.620685534088946, "navigation.courseRhumbline.crossTrackError": 0, "propulsion.main.runTime":1776241 }
|
||||
"status" : "sailing",
|
||||
"metrics" : {"environment.wind.speedTrue": 4.44, "navigation.speedThroughWater": 3.0918118943701245, "performance.velocityMadeGood": 2.9323340761912995, "environment.wind.angleTrueWater": -0.3665191430024964, "environment.depth.belowTransducer": 13.1, "navigation.courseOverGroundMagnetic": 3.620685534088946, "navigation.courseRhumbline.crossTrackError": 0 }
|
||||
},
|
||||
{
|
||||
"time" : "2022-07-30T14:54:28.016Z",
|
||||
@@ -322,7 +334,7 @@
|
||||
"windspeedapparent" : 11.1,
|
||||
"anglespeedapparent" : 88.0,
|
||||
"status" : "sailing",
|
||||
"metrics" : {"environment.wind.speedTrue": 3.1895563635765014, "navigation.speedThroughWater": 0, "performance.velocityMadeGood": 0, "environment.wind.angleTrueWater": 1.8151424224885533, "environment.depth.belowTransducer": 1.67, "navigation.courseOverGroundMagnetic": 3.1290262836898832, "navigation.courseRhumbline.crossTrackError": 0}
|
||||
"metrics" : {"environment.wind.speedTrue": 3.1895563635765014, "navigation.speedThroughWater": 0, "performance.velocityMadeGood": 0, "environment.wind.angleTrueWater": 1.8151424224885533, "environment.depth.belowTransducer": 1.67, "navigation.courseOverGroundMagnetic": 3.1290262836898832, "navigation.courseRhumbline.crossTrackError": 0 }
|
||||
},
|
||||
{
|
||||
"time" : "2022-07-30T15:19:28.467Z",
|
||||
@@ -348,6 +360,18 @@
|
||||
"status" : "moored",
|
||||
"metrics" : {"environment.wind.speedTrue": 0, "navigation.speedThroughWater": 0, "performance.velocityMadeGood": 0, "environment.wind.angleTrueWater": 0.7853981635767779, "environment.depth.belowTransducer": 1.65, "navigation.courseOverGroundMagnetic": 4.206068965341505, "navigation.courseRhumbline.crossTrackError": 0}
|
||||
},
|
||||
{
|
||||
"time" : "2022-07-30T15:20:28.467Z",
|
||||
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
|
||||
"latitude" : 59.97688333333333,
|
||||
"longitude" : 23.4321,
|
||||
"speedoverground" : 0.0,
|
||||
"courseovergroundtrue" : 241.0,
|
||||
"windspeedapparent" : 4.3,
|
||||
"anglespeedapparent" : 74.0,
|
||||
"status" : "sailing",
|
||||
"metrics" : {"environment.wind.speedTrue": 0, "navigation.speedThroughWater": 0, "performance.velocityMadeGood": 0, "environment.wind.angleTrueWater": 0.7853981635767779, "environment.depth.belowTransducer": 1.65, "navigation.courseOverGroundMagnetic": 4.206068965341505, "navigation.courseRhumbline.crossTrackError": 0, "propulsion.main.runTime":1776251}
|
||||
},
|
||||
{
|
||||
"time" : "2022-07-30T15:21:28.467Z",
|
||||
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
|
||||
@@ -357,7 +381,7 @@
|
||||
"courseovergroundtrue" : 241.0,
|
||||
"windspeedapparent" : 4.3,
|
||||
"anglespeedapparent" : 74.0,
|
||||
"status" : "moored",
|
||||
"status" : "sailing",
|
||||
"metrics" : {"environment.wind.speedTrue": 0, "navigation.speedThroughWater": 0, "performance.velocityMadeGood": 0, "environment.wind.angleTrueWater": 0.7853981635767779, "environment.depth.belowTransducer": 1.65, "navigation.courseOverGroundMagnetic": 4.206068965341505, "navigation.courseRhumbline.crossTrackError": 0}
|
||||
},
|
||||
{
|
||||
@@ -609,7 +633,31 @@
|
||||
"courseovergroundtrue" : 122.0,
|
||||
"windspeedapparent" : 7.2,
|
||||
"anglespeedapparent" : 10.0,
|
||||
"status" : "unknown",
|
||||
"metrics" : {}
|
||||
},
|
||||
{
|
||||
"time" : "2022-07-30T15:41:28.867Z",
|
||||
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
|
||||
"latitude" : 59.86,
|
||||
"longitude" : 23.365766666666666,
|
||||
"speedoverground" : 60.0,
|
||||
"courseovergroundtrue" : 122.0,
|
||||
"windspeedapparent" : 7.2,
|
||||
"anglespeedapparent" : 10.0,
|
||||
"status" : "anchored",
|
||||
"metrics" : {"environment.wind.speedTrue": 0.63, "navigation.speedThroughWater": 3.2255674838104293, "performance.velocityMadeGood": -2.242978345998959, "environment.wind.angleTrueWater": 2.3038346131585485, "environment.depth.belowTransducer": 17.73, "navigation.courseOverGroundMagnetic": 2.129127154994025, "navigation.courseRhumbline.crossTrackError": 0}
|
||||
"metrics" : {}
|
||||
},
|
||||
{
|
||||
"time" : "2022-07-30T15:41:28.867Z",
|
||||
"client_id" : "vessels.urn:mrn:imo:mmsi:123456789",
|
||||
"latitude" : 59.86,
|
||||
"longitude" : 23.365766666666666,
|
||||
"speedoverground" : 0.0,
|
||||
"courseovergroundtrue" : 122.0,
|
||||
"windspeedapparent" : 7.2,
|
||||
"anglespeedapparent" : 10.0,
|
||||
"status" : "anchored",
|
||||
"metrics" : {}
|
||||
}
|
||||
]}
|
||||
|
20
tests/sql/anonymous.sql
Normal file
@@ -0,0 +1,20 @@
|
||||
---------------------------------------------------------------------------
|
||||
-- Listing
|
||||
--
|
||||
|
||||
-- List current database
|
||||
select current_database();
|
||||
|
||||
-- connect to the DB
|
||||
\c signalk
|
||||
|
||||
-- output display format
|
||||
\x on
|
||||
|
||||
\echo 'Validate anonymous access'
|
||||
SELECT api.ispublic_fn('kapla', 'public_test');
|
||||
SELECT api.ispublic_fn('kapla', 'public_logs_list');
|
||||
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');
|
26
tests/sql/anonymous.sql.output
Normal file
@@ -0,0 +1,26 @@
|
||||
current_database
|
||||
------------------
|
||||
signalk
|
||||
(1 row)
|
||||
|
||||
You are now connected to database "signalk" as user "username".
|
||||
Expanded display is on.
|
||||
Validate anonymous access
|
||||
-[ RECORD 1 ]--
|
||||
ispublic_fn | f
|
||||
|
||||
-[ RECORD 1 ]--
|
||||
ispublic_fn | f
|
||||
|
||||
-[ RECORD 1 ]--
|
||||
ispublic_fn | t
|
||||
|
||||
-[ RECORD 1 ]--
|
||||
ispublic_fn | f
|
||||
|
||||
-[ RECORD 1 ]--
|
||||
ispublic_fn | t
|
||||
|
||||
-[ RECORD 1 ]--
|
||||
ispublic_fn | f
|
||||
|
@@ -18,7 +18,7 @@ SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
|
||||
\echo 'Insert new api.logbook for badges'
|
||||
INSERT INTO api.logbook
|
||||
(id, active, "name", "_from", "_from_lat", "_from_lng", "_to", "_to_lat", "_to_lng", track_geom, track_geog, track_geojson, "_from_time", "_to_time", distance, duration, avg_speed, max_speed, max_wind_speed, notes, vessel_id)
|
||||
VALUES
|
||||
OVERRIDING SYSTEM VALUE VALUES
|
||||
(nextval('api.logbook_id_seq'), false, 'Tropics Zone', NULL, NULL, NULL, NULL, NULL, NULL, 'SRID=4326;LINESTRING (-63.151124640791096 14.01074681627324, -77.0912026418618 12.870995731013664)'::public.geometry, NULL, NULL, NOW(), NOW(), 123, NULL, NULL, NULL, NULL, NULL, current_setting('vessel.id', false)),
|
||||
(nextval('api.logbook_id_seq'), false, 'Alaska Zone', NULL, NULL, NULL, NULL, NULL, NULL, 'SRID=4326;LINESTRING (-143.5773697471158 59.4404631255976, -152.35402122385003 56.58243132943173)'::public.geometry, NULL, NULL, NOW(), NOW(), 1234, NULL, NULL, NULL, NULL, NULL, current_setting('vessel.id', false));
|
||||
|
||||
@@ -27,10 +27,10 @@ SELECT set_config('user.email', 'demo+kapla@openplotter.cloud', false);
|
||||
--SELECT set_config('vessel.client_id', 'vessels.urn:mrn:imo:mmsi:123456789', false);
|
||||
|
||||
\echo 'Process badge'
|
||||
SELECT badges_logbook_fn(5);
|
||||
SELECT badges_logbook_fn(6);
|
||||
SELECT badges_geom_fn(5);
|
||||
SELECT badges_geom_fn(6);
|
||||
SELECT badges_logbook_fn(5,NOW()::TEXT);
|
||||
SELECT badges_logbook_fn(6,NOW()::TEXT);
|
||||
SELECT badges_geom_fn(5,NOW()::TEXT);
|
||||
SELECT badges_geom_fn(6,NOW()::TEXT);
|
||||
|
||||
\echo 'Check badges for user'
|
||||
SELECT jsonb_object_keys ( a.preferences->'badges' ) FROM auth.accounts a;
|
||||
@@ -53,10 +53,10 @@ SELECT
|
||||
|
||||
\echo 'Insert new api.moorages for badges'
|
||||
INSERT INTO api.moorages
|
||||
(id,"name",country,stay_id,stay_code,stay_duration,reference_count,latitude,longitude,geog,home_flag,notes,vessel_id)
|
||||
VALUES
|
||||
(5,'Badge Mooring Pro',NULL,5,3,'11 days 00:39:56.418',1,NULL,NULL,NULL,false,'Badge Mooring Pro',current_setting('vessel.id', false)),
|
||||
(6,'Badge Anchormaster',NULL,5,2,'26 days 00:49:56.418',1,NULL,NULL,NULL,false,'Badge Anchormaster',current_setting('vessel.id', false));
|
||||
(id,"name",country,stay_code,stay_duration,reference_count,latitude,longitude,geog,home_flag,notes,vessel_id)
|
||||
OVERRIDING SYSTEM VALUE VALUES
|
||||
(8,'Badge Mooring Pro',NULL,3,'11 days 00:39:56.418',1,NULL,NULL,NULL,false,'Badge Mooring Pro',current_setting('vessel.id', false)),
|
||||
(9,'Badge Anchormaster',NULL,2,'26 days 00:49:56.418',1,NULL,NULL,NULL,false,'Badge Anchormaster',current_setting('vessel.id', false));
|
||||
|
||||
\echo 'Set config'
|
||||
SELECT set_config('user.email', 'demo+aava@openplotter.cloud', false);
|
||||
|
@@ -25,13 +25,13 @@ SELECT set_config('vessel.id', :'vessel_id', false) IS NOT NULL as vessel_id;
|
||||
\echo 'logbook'
|
||||
SELECT count(*) FROM api.logbook WHERE vessel_id = current_setting('vessel.id', false);
|
||||
\echo 'logbook'
|
||||
SELECT name,_from_time IS NOT NULL AS _from_time,_to_time IS NOT NULL AS _to_time, track_geojson IS NOT NULL AS track_geojson, track_gpx IS NOT NULL AS track_gpx, track_geom, distance,duration,avg_speed,max_speed,max_wind_speed,notes,extra FROM api.logbook WHERE vessel_id = current_setting('vessel.id', false);
|
||||
SELECT name,_from_time IS NOT NULL AS _from_time,_to_time IS NOT NULL AS _to_time, track_geojson IS NOT NULL AS track_geojson, track_geom, distance,duration,avg_speed,max_speed,max_wind_speed,notes,extra FROM api.logbook WHERE vessel_id = current_setting('vessel.id', false);
|
||||
|
||||
-- Test stays for user
|
||||
\echo 'stays'
|
||||
SELECT count(*) FROM api.stays WHERE vessel_id = current_setting('vessel.id', false);
|
||||
\echo 'stays'
|
||||
SELECT active,name,geog,stay_code FROM api.stays WHERE vessel_id = current_setting('vessel.id', false);
|
||||
SELECT active,name IS NOT NULL AS name,geog,stay_code FROM api.stays WHERE vessel_id = current_setting('vessel.id', false);
|
||||
|
||||
-- Test event logs view for user
|
||||
\echo 'eventlogs_view'
|
||||
@@ -57,3 +57,20 @@ SELECT stats_logs_fn->'name' AS name,
|
||||
FROM stats_jsonb;
|
||||
DROP TABLE stats_jsonb;
|
||||
SELECT api.stats_logs_fn('2022-01-01'::text,'2022-06-12'::text);
|
||||
|
||||
-- Update logbook observations
|
||||
\echo 'update_logbook_observations_fn'
|
||||
SELECT extra FROM api.logbook l WHERE id = 1 AND vessel_id = current_setting('vessel.id', false);
|
||||
SELECT api.update_logbook_observations_fn(1, '{"observations":{"cloudCoverage":1}}'::TEXT);
|
||||
SELECT extra FROM api.logbook l WHERE id = 1 AND vessel_id = current_setting('vessel.id', false);
|
||||
|
||||
-- Check export
|
||||
--\echo 'check logbook export fn'
|
||||
--SELECT api.export_logbook_geojson_fn(1);
|
||||
--SELECT api.export_logbook_gpx_fn(1);
|
||||
--SELECT api.export_logbook_kml_fn(1);
|
||||
|
||||
-- Check history
|
||||
--\echo 'monitoring history fn'
|
||||
--select api.monitoring_history_fn();
|
||||
--select api.monitoring_history_fn('24');
|
||||
|
@@ -16,30 +16,28 @@ logbook
|
||||
count | 2
|
||||
|
||||
logbook
|
||||
-[ RECORD 1 ]--+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
name | Bollsta to Slottsbacken
|
||||
-[ RECORD 1 ]--+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
name | Pojoviken to Norra hamnen
|
||||
_from_time | t
|
||||
_to_time | t
|
||||
track_geojson | t
|
||||
track_gpx | t
|
||||
track_geom | 0102000020E61000001A00000020D26F5F0786374030BB270F0B094E400C6E7ED60F843740AA60545227084E40D60FC48C03823740593CE27D42074E407B39D9F322803740984C158C4A064E4091ED7C3F357E3740898BB63D54054E40A8A1208B477C37404BA3DC9059044E404C5CB4EDA17A3740C4F856115B034E40A9A44E4013793740D8F0F44A59024E40E4839ECDAA773740211FF46C56014E405408D147067637408229F03B73004E40787AA52C43743740F90FE9B7AFFF4D40F8098D4D18723740C217265305FF4D4084E82303537037409A2D464AA0FE4D4022474DCE636F37402912396A72FE4D408351499D806E374088CFB02B40FE4D4076711B0DE06D3740B356C7040FFE4D404EAC66B0BC6E374058A835CD3BFE4D40D7A3703D0A6F3740D3E10EC15EFE4D4087602F277B6E3740A779C7293AFE4D4087602F277B6E3740A779C7293AFE4D402063EE5A426E3740B5A679C729FE4D40381DEE10EC6D37409ECA7C1A0AFE4D40E2C46A06CB6B37400A43F7BF36FD4D4075931804566E3740320BDAD125FD4D409A2D464AA06E37404A5658830AFD4D40029A081B9E6E37404A5658830AFD4D40
|
||||
distance | 7.17
|
||||
duration | PT25M
|
||||
avg_speed | 3.6961538461538455
|
||||
track_geom | 0102000020E61000001C000000B0DEBBE0E68737404DA938FBF0094E40B0DEBBE0E68737404DA938FBF0094E4020D26F5F0786374030BB270F0B094E400C6E7ED60F843740AA60545227084E40D60FC48C03823740593CE27D42074E407B39D9F322803740984C158C4A064E4091ED7C3F357E3740898BB63D54054E40A8A1208B477C37404BA3DC9059044E404C5CB4EDA17A3740C4F856115B034E40A9A44E4013793740D8F0F44A59024E40E4839ECDAA773740211FF46C56014E405408D147067637408229F03B73004E40787AA52C43743740F90FE9B7AFFF4D40F8098D4D18723740C217265305FF4D4084E82303537037409A2D464AA0FE4D4022474DCE636F37402912396A72FE4D408351499D806E374088CFB02B40FE4D4076711B0DE06D3740B356C7040FFE4D404EAC66B0BC6E374058A835CD3BFE4D40D7A3703D0A6F3740D3E10EC15EFE4D4087602F277B6E3740A779C7293AFE4D4087602F277B6E3740A779C7293AFE4D402063EE5A426E3740B5A679C729FE4D40381DEE10EC6D37409ECA7C1A0AFE4D40E2C46A06CB6B37400A43F7BF36FD4D4075931804566E3740320BDAD125FD4D409A2D464AA06E37404A5658830AFD4D40029A081B9E6E37404A5658830AFD4D40
|
||||
distance | 7.6447
|
||||
duration | PT27M
|
||||
avg_speed | 3.6357142857142852
|
||||
max_speed | 6.1
|
||||
max_wind_speed | 22.1
|
||||
notes | new log note
|
||||
notes |
|
||||
extra | {"metrics": {"propulsion.main.runTime": 10}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": -1}}
|
||||
-[ RECORD 2 ]--+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
name | Knipan to Ekenäs
|
||||
-[ RECORD 2 ]--+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
name | Norra hamnen to Ekenäs
|
||||
_from_time | t
|
||||
_to_time | t
|
||||
track_geojson | t
|
||||
track_gpx | t
|
||||
track_geom | 0102000020E6100000130000004806A6C0EF6C3740DA1B7C6132FD4D40FE65F7E461693740226C787AA5FC4D407DD3E10EC1663740B29DEFA7C6FB4D40898BB63D5465374068479724BCFA4D409A5271F6E1633740B6847CD0B3F94D40431CEBE236623740E9263108ACF84D402C6519E2585F37407E678EBFC7F74D4096218E75715B374027C5B45C23F74D402AA913D044583740968DE1C46AF64D405AF5B9DA8A5537407BEF829B9FF54D407449C2ABD253374086C954C1A8F44D407D1A0AB278543740F2B0506B9AF34D409D11A5BDC15737406688635DDCF24D4061C3D32B655937402CAF6F3ADCF14D408988888888583740B3319C58CDF04D4021FAC8C0145837408C94405DB7EF4D40B8F9593F105B37403DC0804BEDEE4D40DE4C5FE2A25D3740AE47E17A14EE4D40DE4C5FE2A25D3740AE47E17A14EE4D40
|
||||
distance | 8.6862
|
||||
duration | PT18M
|
||||
avg_speed | 6.026315789473684
|
||||
track_geom | 0102000020E610000015000000029A081B9E6E37404A5658830AFD4D40029A081B9E6E37404A5658830AFD4D404806A6C0EF6C3740DA1B7C6132FD4D40FE65F7E461693740226C787AA5FC4D407DD3E10EC1663740B29DEFA7C6FB4D40898BB63D5465374068479724BCFA4D409A5271F6E1633740B6847CD0B3F94D40431CEBE236623740E9263108ACF84D402C6519E2585F37407E678EBFC7F74D4096218E75715B374027C5B45C23F74D402AA913D044583740968DE1C46AF64D405AF5B9DA8A5537407BEF829B9FF54D407449C2ABD253374086C954C1A8F44D407D1A0AB278543740F2B0506B9AF34D409D11A5BDC15737406688635DDCF24D4061C3D32B655937402CAF6F3ADCF14D408988888888583740B3319C58CDF04D4021FAC8C0145837408C94405DB7EF4D40B8F9593F105B37403DC0804BEDEE4D40DE4C5FE2A25D3740AE47E17A14EE4D40DE4C5FE2A25D3740AE47E17A14EE4D40
|
||||
distance | 8.8968
|
||||
duration | PT20M
|
||||
avg_speed | 5.4523809523809526
|
||||
max_speed | 6.5
|
||||
max_wind_speed | 37.2
|
||||
notes |
|
||||
@@ -51,24 +49,24 @@ count | 3
|
||||
|
||||
stays
|
||||
-[ RECORD 1 ]-------------------------------------------------
|
||||
active | f
|
||||
name | Bollsta
|
||||
geog | 0101000020E6100000B0DEBBE0E68737404DA938FBF0094E40
|
||||
active | t
|
||||
name | f
|
||||
geog |
|
||||
stay_code | 2
|
||||
-[ RECORD 2 ]-------------------------------------------------
|
||||
active | f
|
||||
name | Slottsbacken
|
||||
geog | 0101000020E6100000029A081B9E6E37404A5658830AFD4D40
|
||||
stay_code | 1
|
||||
-[ RECORD 3 ]-------------------------------------------------
|
||||
active | t
|
||||
name | Ekenäs
|
||||
geog | 0101000020E6100000DE4C5FE2A25D3740AE47E17A14EE4D40
|
||||
name | t
|
||||
geog | 0101000020E6100000B0DEBBE0E68737404DA938FBF0094E40
|
||||
stay_code | 2
|
||||
-[ RECORD 3 ]-------------------------------------------------
|
||||
active | f
|
||||
name | t
|
||||
geog | 0101000020E6100000029A081B9E6E37404A5658830AFD4D40
|
||||
stay_code | 4
|
||||
|
||||
eventlogs_view
|
||||
-[ RECORD 1 ]
|
||||
count | 13
|
||||
count | 12
|
||||
|
||||
stats_logs_fn
|
||||
SELECT 1
|
||||
@@ -76,11 +74,11 @@ SELECT 1
|
||||
name | "kapla"
|
||||
count | 4
|
||||
max_speed | 7.1
|
||||
max_distance | 8.6862
|
||||
max_distance | 8.8968
|
||||
max_duration | "PT1H11M"
|
||||
?column? | 3
|
||||
?column? | 29.2865
|
||||
?column? | "PT2H37M"
|
||||
?column? | 30.1154
|
||||
?column? | "PT2H43M"
|
||||
?column? | 44.2
|
||||
?column? | 2
|
||||
?column? | 4
|
||||
@@ -92,3 +90,13 @@ DROP TABLE
|
||||
-[ RECORD 1 ]-+-
|
||||
stats_logs_fn |
|
||||
|
||||
update_logbook_observations_fn
|
||||
-[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------
|
||||
extra | {"metrics": {"propulsion.main.runTime": 10}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": -1}}
|
||||
|
||||
-[ RECORD 1 ]------------------+--
|
||||
update_logbook_observations_fn | t
|
||||
|
||||
-[ RECORD 1 ]---------------------------------------------------------------------------------------------------------------
|
||||
extra | {"metrics": {"propulsion.main.runTime": 10}, "observations": {"seaState": -1, "visibility": -1, "cloudCoverage": 1}}
|
||||
|
||||
|
@@ -12,9 +12,14 @@ select current_database();
|
||||
\x on
|
||||
|
||||
-- Check the number of process pending
|
||||
\echo 'Check the number of process pending'
|
||||
-- Should be 22
|
||||
SELECT count(*) as jobs from public.process_queue pq where pq.processed is null;
|
||||
--set role scheduler
|
||||
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;
|
||||
|
||||
-- Check the number of metrics entries
|
||||
\echo 'Check the number of metrics entries'
|
||||
SELECT count(*) as metrics_count from api.metrics;
|
||||
|
@@ -5,12 +5,17 @@
|
||||
|
||||
You are now connected to database "signalk" as user "username".
|
||||
Expanded display is on.
|
||||
Check the number of process pending
|
||||
-[ RECORD 1 ]
|
||||
jobs | 28
|
||||
jobs | 26
|
||||
|
||||
-[ RECORD 1 ]-+-
|
||||
run_cron_jobs |
|
||||
|
||||
-[ RECORD 1 ]----+--
|
||||
any_pending_jobs | 0
|
||||
any_pending_jobs | 2
|
||||
|
||||
Check the number of metrics entries
|
||||
-[ RECORD 1 ]-+----
|
||||
metrics_count | 172
|
||||
|
||||
|
@@ -23,7 +23,7 @@ SELECT current_user, current_setting('user.email', true), current_setting('vesse
|
||||
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;
|
||||
|
||||
\echo 'auth.accounts details'
|
||||
SELECT a.userid IS NOT NULL AS userid, 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;
|
||||
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;
|
||||
\echo 'auth.vessels details'
|
||||
--SELECT 'SELECT ' || STRING_AGG('v.' || column_name, ', ') || ' FROM auth.vessels AS v' FROM information_schema.columns WHERE table_name = 'vessels' AND table_schema = 'auth' AND column_name NOT IN ('created_at', 'updated_at');
|
||||
SELECT v.vessel_id IS NOT NULL AS vessel_id, v.owner_email, v.mmsi, v.name, v.role FROM auth.vessels AS v;
|
||||
@@ -60,12 +60,12 @@ SELECT m.id, m.name, m.mmsi, m.client_id, m.length, m.beam, m.height, m.ship_typ
|
||||
\echo 'api.logs_view'
|
||||
--SELECT * FROM api.logbook l;
|
||||
--SELECT * FROM api.logs_view l;
|
||||
SELECT l.id, "Name", "From", "To", "Distance", "Duration" FROM api.logs_view AS l;
|
||||
SELECT l.id, l.name, l.from, l.to, l.distance, l.duration, l._from_moorage_id, l._to_moorage_id FROM api.logs_view AS l;
|
||||
--SELECT * FROM api.log_view l;
|
||||
|
||||
\echo 'api.stays'
|
||||
--SELECT * FROM api.stays s;
|
||||
SELECT m.id, m.vessel_id IS NOT NULL AS vessel_id, m.active, m.name, m.latitude, m.longitude, m.geog, m.arrived IS NOT NULL AS arrived, m.departed IS NOT NULL AS departed, m.duration, m.stay_code, m.notes FROM api.stays AS m;
|
||||
SELECT m.id, m.vessel_id IS NOT NULL AS vessel_id, m.moorage_id, m.active, m.name IS NOT NULL AS name, m.latitude, m.longitude, m.geog, m.arrived IS NOT NULL AS arrived, m.departed IS NOT NULL AS departed, m.duration, m.stay_code, m.notes FROM api.stays AS m;
|
||||
|
||||
\echo 'stays_view'
|
||||
--SELECT * FROM api.stays_view s;
|
||||
@@ -73,7 +73,7 @@ SELECT m.id, m.name IS NOT NULL AS name, m.moorage, m.moorage_id, m.duration, m.
|
||||
|
||||
\echo 'api.moorages'
|
||||
--SELECT * FROM api.moorages m;
|
||||
SELECT m.id, m.vessel_id IS NOT NULL AS vessel_id, m.name, m.country, m.stay_id, m.stay_code, m.stay_duration, m.reference_count, m.latitude, m.longitude, m.geog, m.home_flag, m.notes FROM api.moorages AS m;
|
||||
SELECT m.id, m.vessel_id IS NOT NULL AS vessel_id, m.name, m.country, m.stay_code, m.stay_duration, m.reference_count, m.latitude, m.longitude, m.geog, m.home_flag, m.notes FROM api.moorages AS m;
|
||||
|
||||
\echo 'api.moorages_view'
|
||||
SELECT * FROM api.moorages_view s;
|
||||
|
@@ -15,29 +15,27 @@ current_setting |
|
||||
|
||||
link vessel and user based on current_setting
|
||||
-[ RECORD 1 ]----------------------------------------------------------------
|
||||
name | kapla
|
||||
client_id | vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd
|
||||
-[ RECORD 2 ]----------------------------------------------------------------
|
||||
name | aava
|
||||
client_id | vessels.urn:mrn:imo:mmsi:787654321
|
||||
-[ RECORD 2 ]----------------------------------------------------------------
|
||||
name | kapla
|
||||
client_id | vessels.urn:mrn:signalk:uuid:5b4f7543-7153-4840-b139-761310b242fd
|
||||
|
||||
auth.accounts details
|
||||
-[ RECORD 1 ]-----+-----------------------------
|
||||
userid | t
|
||||
user_id | t
|
||||
email | demo+kapla@openplotter.cloud
|
||||
first | First_kapla
|
||||
last | Last_kapla
|
||||
email | demo+aava@openplotter.cloud
|
||||
first | first_aava
|
||||
last | last_aava
|
||||
pass | t
|
||||
role | user_role
|
||||
telegram |
|
||||
pushover_user_key |
|
||||
-[ RECORD 2 ]-----+-----------------------------
|
||||
userid | t
|
||||
user_id | t
|
||||
email | demo+aava@openplotter.cloud
|
||||
first | first_aava
|
||||
last | last_aava
|
||||
email | demo+kapla@openplotter.cloud
|
||||
first | First_kapla
|
||||
last | Last_kapla
|
||||
pass | t
|
||||
role | user_role
|
||||
telegram |
|
||||
@@ -125,80 +123,87 @@ time | t
|
||||
active | t
|
||||
|
||||
api.logs_view
|
||||
-[ RECORD 1 ]--------------
|
||||
id | 2
|
||||
Name | Knipan to Ekenäs
|
||||
From | Knipan
|
||||
To | Ekenäs
|
||||
Distance | 8.6862
|
||||
Duration | PT18M
|
||||
-[ RECORD 2 ]--------------
|
||||
id | 1
|
||||
Name | patch log name 3
|
||||
From | Bollsta
|
||||
To | Slottsbacken
|
||||
Distance | 7.17
|
||||
Duration | PT25M
|
||||
-[ RECORD 1 ]----+-----------------------
|
||||
id | 2
|
||||
name | Norra hamnen to Ekenäs
|
||||
from | Norra hamnen
|
||||
to | Ekenäs
|
||||
distance | 8.8968
|
||||
duration | PT20M
|
||||
_from_moorage_id | 2
|
||||
_to_moorage_id | 3
|
||||
-[ RECORD 2 ]----+-----------------------
|
||||
id | 1
|
||||
name | patch log name 3
|
||||
from | patch moorage name 3
|
||||
to | Norra hamnen
|
||||
distance | 7.6447
|
||||
duration | PT27M
|
||||
_from_moorage_id | 1
|
||||
_to_moorage_id | 2
|
||||
|
||||
api.stays
|
||||
-[ RECORD 1 ]-------------------------------------------------
|
||||
id | 1
|
||||
vessel_id | t
|
||||
active | f
|
||||
name | patch stay name 3
|
||||
latitude | 60.077666666666666
|
||||
longitude | 23.530866666666668
|
||||
geog | 0101000020E6100000B0DEBBE0E68737404DA938FBF0094E40
|
||||
arrived | t
|
||||
departed | t
|
||||
duration |
|
||||
stay_code | 2
|
||||
notes | new stay note 3
|
||||
-[ RECORD 2 ]-------------------------------------------------
|
||||
id | 2
|
||||
vessel_id | t
|
||||
active | f
|
||||
name | Slottsbacken
|
||||
latitude | 59.97688333333333
|
||||
longitude | 23.4321
|
||||
geog | 0101000020E6100000029A081B9E6E37404A5658830AFD4D40
|
||||
arrived | t
|
||||
departed | t
|
||||
duration |
|
||||
stay_code | 1
|
||||
notes |
|
||||
-[ RECORD 3 ]-------------------------------------------------
|
||||
id | 3
|
||||
vessel_id | t
|
||||
active | t
|
||||
name | Ekenäs
|
||||
latitude | 59.86
|
||||
longitude | 23.365766666666666
|
||||
geog | 0101000020E6100000DE4C5FE2A25D3740AE47E17A14EE4D40
|
||||
arrived | t
|
||||
departed | f
|
||||
duration |
|
||||
stay_code | 2
|
||||
notes |
|
||||
-[ RECORD 1 ]--------------------------------------------------
|
||||
id | 3
|
||||
vessel_id | t
|
||||
moorage_id |
|
||||
active | t
|
||||
name | f
|
||||
latitude | 59.86
|
||||
longitude | 23.365766666666666
|
||||
geog |
|
||||
arrived | t
|
||||
departed | f
|
||||
duration |
|
||||
stay_code | 2
|
||||
notes |
|
||||
-[ RECORD 2 ]--------------------------------------------------
|
||||
id | 1
|
||||
vessel_id | t
|
||||
moorage_id | 1
|
||||
active | f
|
||||
name | t
|
||||
latitude | 60.077666666666666
|
||||
longitude | 23.530866666666668
|
||||
geog | 0101000020E6100000B0DEBBE0E68737404DA938FBF0094E40
|
||||
arrived | t
|
||||
departed | t
|
||||
duration | PT1M
|
||||
stay_code | 2
|
||||
notes | new stay note 3
|
||||
-[ RECORD 3 ]--------------------------------------------------
|
||||
id | 2
|
||||
vessel_id | t
|
||||
moorage_id | 2
|
||||
active | f
|
||||
name | t
|
||||
latitude | 59.97688333333333
|
||||
longitude | 23.4321
|
||||
geog | 0101000020E6100000029A081B9E6E37404A5658830AFD4D40
|
||||
arrived | t
|
||||
departed | t
|
||||
duration | PT2M
|
||||
stay_code | 4
|
||||
notes |
|
||||
|
||||
stays_view
|
||||
-[ RECORD 1 ]+------------------
|
||||
-[ RECORD 1 ]+---------------------
|
||||
id | 2
|
||||
name | t
|
||||
moorage | Slottsbacken
|
||||
moorage | Norra hamnen
|
||||
moorage_id | 2
|
||||
duration | PT3M
|
||||
stayed_at | Unknown
|
||||
stayed_at_id | 1
|
||||
duration | PT2M
|
||||
stayed_at | Dock
|
||||
stayed_at_id | 4
|
||||
arrived | t
|
||||
departed | t
|
||||
notes |
|
||||
-[ RECORD 2 ]+------------------
|
||||
-[ RECORD 2 ]+---------------------
|
||||
id | 1
|
||||
name | t
|
||||
moorage | patch stay name 3
|
||||
moorage | patch moorage name 3
|
||||
moorage_id | 1
|
||||
duration | PT2M
|
||||
duration | PT1M
|
||||
stayed_at | Anchor
|
||||
stayed_at_id | 2
|
||||
arrived | t
|
||||
@@ -210,44 +215,57 @@ api.moorages
|
||||
id | 1
|
||||
vessel_id | t
|
||||
name | patch moorage name 3
|
||||
country | fi
|
||||
stay_id | 1
|
||||
country |
|
||||
stay_code | 2
|
||||
stay_duration | PT2M
|
||||
stay_duration | PT1M
|
||||
reference_count | 1
|
||||
latitude | 60.077666666666666
|
||||
longitude | 23.530866666666668
|
||||
geog | 0101000020E6100000B0DEBBE0E68737404DA938FBF0094E40
|
||||
latitude | 60.0776666666667
|
||||
longitude | 23.5308666666667
|
||||
geog | 0101000020E6100000B9DEBBE0E687374052A938FBF0094E40
|
||||
home_flag | t
|
||||
notes | new moorage note 3
|
||||
-[ RECORD 2 ]---+---------------------------------------------------
|
||||
id | 2
|
||||
vessel_id | t
|
||||
name | Slottsbacken
|
||||
country | fi
|
||||
stay_id | 2
|
||||
stay_code | 1
|
||||
stay_duration | PT3M
|
||||
reference_count | 1
|
||||
latitude | 59.97688333333333
|
||||
name | Norra hamnen
|
||||
country |
|
||||
stay_code | 4
|
||||
stay_duration | PT2M
|
||||
reference_count | 2
|
||||
latitude | 59.9768833333333
|
||||
longitude | 23.4321
|
||||
geog | 0101000020E6100000029A081B9E6E37404A5658830AFD4D40
|
||||
geog | 0101000020E6100000029A081B9E6E3740455658830AFD4D40
|
||||
home_flag | f
|
||||
notes |
|
||||
-[ RECORD 3 ]---+---------------------------------------------------
|
||||
id | 3
|
||||
vessel_id | t
|
||||
name | Ekenäs
|
||||
country | fi
|
||||
stay_code | 1
|
||||
stay_duration |
|
||||
reference_count | 1
|
||||
latitude | 59.86
|
||||
longitude | 23.3657666666667
|
||||
geog | 0101000020E6100000E84C5FE2A25D3740AE47E17A14EE4D40
|
||||
home_flag | f
|
||||
notes |
|
||||
|
||||
api.moorages_view
|
||||
-[ RECORD 1 ]-------+---------------------
|
||||
id | 2
|
||||
moorage | Norra hamnen
|
||||
default_stay | Dock
|
||||
default_stay_id | 4
|
||||
total_stay | 0
|
||||
total_duration | PT2M
|
||||
arrivals_departures | 2
|
||||
-[ RECORD 2 ]-------+---------------------
|
||||
id | 1
|
||||
moorage | patch moorage name 3
|
||||
default_stay | Anchor
|
||||
default_stay_id | 2
|
||||
total_stay | 0
|
||||
arrivals_departures | 1
|
||||
-[ RECORD 2 ]-------+---------------------
|
||||
id | 2
|
||||
moorage | Slottsbacken
|
||||
default_stay | Unknown
|
||||
default_stay_id | 1
|
||||
total_stay | 0
|
||||
total_duration | PT1M
|
||||
arrivals_departures | 1
|
||||
|
||||
|
@@ -22,15 +22,15 @@ count | 21
|
||||
|
||||
Test monitoring_view3 for user
|
||||
-[ RECORD 1 ]
|
||||
count | 3682
|
||||
count | 3736
|
||||
|
||||
Test monitoring_voltage for user
|
||||
-[ RECORD 1 ]
|
||||
count | 46
|
||||
count | 47
|
||||
|
||||
Test monitoring_temperatures for user
|
||||
-[ RECORD 1 ]
|
||||
count | 119
|
||||
count | 120
|
||||
|
||||
Test monitoring_humidity for user
|
||||
-[ RECORD 1 ]
|
||||
|
@@ -81,6 +81,10 @@ select * from pg_policies;
|
||||
SELECT public.reverse_geocode_py_fn('nominatim', 1.4440116666666667, 38.82985166666667);
|
||||
\echo 'Test geoip reverse_geoip_py_fn'
|
||||
--SELECT reverse_geoip_py_fn('62.74.13.231');
|
||||
\echo 'Test opverpass API overpass_py_fn'
|
||||
SELECT public.overpass_py_fn(2.19917, 41.386873333333334); -- Port Olimpic
|
||||
SELECT public.overpass_py_fn(1.92574333333, 41.258915); -- Port de la Ginesta
|
||||
SELECT public.overpass_py_fn(23.4321, 59.9768833333333); -- Norra hamnen
|
||||
|
||||
-- List details product versions
|
||||
SELECT api.versions_fn();
|
||||
|
@@ -6,10 +6,10 @@
|
||||
You are now connected to database "signalk" as user "username".
|
||||
Expanded display is on.
|
||||
-[ RECORD 1 ]--+-------------------------------
|
||||
server_version | 15.4 (Debian 15.4-2.pgdg110+1)
|
||||
server_version | 16.1 (Debian 16.1-1.pgdg110+1)
|
||||
|
||||
-[ RECORD 1 ]--------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
postgis_full_version | POSTGIS="3.4.0 0874ea3" [EXTENSION] PGSQL="150" GEOS="3.9.0-CAPI-1.16.2" PROJ="7.2.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" LIBXML="2.9.10" LIBJSON="0.15" LIBPROTOBUF="1.3.3" WAGYU="0.5.0 (Internal)"
|
||||
postgis_full_version | POSTGIS="3.4.1 ca035b9" [EXTENSION] PGSQL="160" GEOS="3.9.0-CAPI-1.16.2" PROJ="7.2.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" LIBXML="2.9.10" LIBJSON="0.15" LIBPROTOBUF="1.3.3" WAGYU="0.5.0 (Internal)"
|
||||
|
||||
-[ RECORD 1 ]--------------------------------------------------------------------------------------
|
||||
Name | citext
|
||||
@@ -48,12 +48,12 @@ Schema | pg_catalog
|
||||
Description | PL/Python3U untrusted procedural language
|
||||
-[ RECORD 8 ]--------------------------------------------------------------------------------------
|
||||
Name | postgis
|
||||
Version | 3.4.0
|
||||
Version | 3.4.1
|
||||
Schema | public
|
||||
Description | PostGIS geometry and geography spatial types and functions
|
||||
-[ RECORD 9 ]--------------------------------------------------------------------------------------
|
||||
Name | timescaledb
|
||||
Version | 2.12.0
|
||||
Version | 2.13.1
|
||||
Schema | public
|
||||
Description | Enables scalable inserts and complex queries for time-series data (Community Edition)
|
||||
-[ RECORD 10 ]-------------------------------------------------------------------------------------
|
||||
@@ -96,24 +96,24 @@ laninline | 0
|
||||
lanvalidator | 2248
|
||||
lanacl |
|
||||
-[ RECORD 4 ]-+-----------
|
||||
oid | 13542
|
||||
oid | 13545
|
||||
lanname | plpgsql
|
||||
lanowner | 10
|
||||
lanispl | t
|
||||
lanpltrusted | t
|
||||
lanplcallfoid | 13539
|
||||
laninline | 13540
|
||||
lanvalidator | 13541
|
||||
lanplcallfoid | 13542
|
||||
laninline | 13543
|
||||
lanvalidator | 13544
|
||||
lanacl |
|
||||
-[ RECORD 5 ]-+-----------
|
||||
oid | 18283
|
||||
oid | 18297
|
||||
lanname | plpython3u
|
||||
lanowner | 10
|
||||
lanispl | t
|
||||
lanpltrusted | t
|
||||
lanplcallfoid | 18280
|
||||
laninline | 18281
|
||||
lanvalidator | 18282
|
||||
lanplcallfoid | 18294
|
||||
laninline | 18295
|
||||
lanvalidator | 18296
|
||||
lanacl |
|
||||
|
||||
-[ RECORD 1 ]+-----------
|
||||
@@ -243,6 +243,8 @@ schema_auth | accounts
|
||||
-[ RECORD 2 ]---------
|
||||
schema_auth | otp
|
||||
-[ RECORD 3 ]---------
|
||||
schema_auth | users
|
||||
-[ RECORD 4 ]---------
|
||||
schema_auth | vessels
|
||||
|
||||
(0 rows)
|
||||
@@ -321,14 +323,14 @@ cmd | ALL
|
||||
qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | true
|
||||
-[ RECORD 9 ]------------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | moorages
|
||||
policyname | admin_all
|
||||
schemaname | auth
|
||||
tablename | vessels
|
||||
policyname | grafana_proxy_role
|
||||
permissive | PERMISSIVE
|
||||
roles | {username}
|
||||
roles | {grafana_auth}
|
||||
cmd | ALL
|
||||
qual | true
|
||||
with_check | true
|
||||
with_check | false
|
||||
-[ RECORD 10 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | metrics
|
||||
@@ -358,6 +360,15 @@ qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | false
|
||||
-[ RECORD 13 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | metrics
|
||||
policyname | api_anonymous_role
|
||||
permissive | PERMISSIVE
|
||||
roles | {api_anonymous}
|
||||
cmd | ALL
|
||||
qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | false
|
||||
-[ RECORD 14 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | logbook
|
||||
policyname | admin_all
|
||||
permissive | PERMISSIVE
|
||||
@@ -365,7 +376,7 @@ roles | {username}
|
||||
cmd | ALL
|
||||
qual | true
|
||||
with_check | true
|
||||
-[ RECORD 14 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 15 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | logbook
|
||||
policyname | api_vessel_role
|
||||
@@ -374,26 +385,8 @@ roles | {vessel_role}
|
||||
cmd | ALL
|
||||
qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | true
|
||||
-[ RECORD 15 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | logbook
|
||||
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 16 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | auth
|
||||
tablename | vessels
|
||||
policyname | grafana_proxy_role
|
||||
permissive | PERMISSIVE
|
||||
roles | {grafana_auth}
|
||||
cmd | ALL
|
||||
qual | true
|
||||
with_check | false
|
||||
-[ RECORD 17 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | auth
|
||||
tablename | accounts
|
||||
policyname | admin_all
|
||||
permissive | PERMISSIVE
|
||||
@@ -401,6 +394,15 @@ roles | {username}
|
||||
cmd | ALL
|
||||
qual | true
|
||||
with_check | true
|
||||
-[ RECORD 17 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | logbook
|
||||
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 18 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | logbook
|
||||
@@ -421,6 +423,15 @@ qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | false
|
||||
-[ RECORD 20 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | logbook
|
||||
policyname | api_anonymous_role
|
||||
permissive | PERMISSIVE
|
||||
roles | {api_anonymous}
|
||||
cmd | ALL
|
||||
qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | false
|
||||
-[ RECORD 21 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | stays
|
||||
policyname | admin_all
|
||||
permissive | PERMISSIVE
|
||||
@@ -428,7 +439,7 @@ roles | {username}
|
||||
cmd | ALL
|
||||
qual | true
|
||||
with_check | true
|
||||
-[ RECORD 21 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 22 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | stays
|
||||
policyname | api_vessel_role
|
||||
@@ -437,7 +448,7 @@ roles | {vessel_role}
|
||||
cmd | ALL
|
||||
qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | true
|
||||
-[ RECORD 22 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 23 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | stays
|
||||
policyname | api_user_role
|
||||
@@ -446,25 +457,43 @@ 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 23 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | stays
|
||||
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 24 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | stays
|
||||
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 25 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | stays
|
||||
policyname | grafana_role
|
||||
permissive | PERMISSIVE
|
||||
roles | {grafana}
|
||||
cmd | ALL
|
||||
qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | false
|
||||
-[ RECORD 25 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 26 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | stays
|
||||
policyname | api_anonymous_role
|
||||
permissive | PERMISSIVE
|
||||
roles | {api_anonymous}
|
||||
cmd | ALL
|
||||
qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | false
|
||||
-[ RECORD 27 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | moorages
|
||||
policyname | admin_all
|
||||
permissive | PERMISSIVE
|
||||
roles | {username}
|
||||
cmd | ALL
|
||||
qual | true
|
||||
with_check | true
|
||||
-[ RECORD 28 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | moorages
|
||||
policyname | api_vessel_role
|
||||
@@ -473,7 +502,7 @@ roles | {vessel_role}
|
||||
cmd | ALL
|
||||
qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | true
|
||||
-[ RECORD 26 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 29 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | moorages
|
||||
policyname | api_user_role
|
||||
@@ -482,7 +511,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 27 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 30 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | moorages
|
||||
policyname | api_scheduler_role
|
||||
@@ -491,7 +520,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 28 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 31 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | moorages
|
||||
policyname | grafana_role
|
||||
@@ -500,7 +529,16 @@ roles | {grafana}
|
||||
cmd | ALL
|
||||
qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | false
|
||||
-[ RECORD 29 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 32 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | api
|
||||
tablename | moorages
|
||||
policyname | api_anonymous_role
|
||||
permissive | PERMISSIVE
|
||||
roles | {api_anonymous}
|
||||
cmd | ALL
|
||||
qual | (vessel_id = current_setting('vessel.id'::text, false))
|
||||
with_check | false
|
||||
-[ RECORD 33 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | auth
|
||||
tablename | vessels
|
||||
policyname | admin_all
|
||||
@@ -509,7 +547,7 @@ roles | {username}
|
||||
cmd | ALL
|
||||
qual | true
|
||||
with_check | true
|
||||
-[ RECORD 30 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 34 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | auth
|
||||
tablename | vessels
|
||||
policyname | api_user_role
|
||||
@@ -518,7 +556,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 31 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 35 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | auth
|
||||
tablename | vessels
|
||||
policyname | grafana_role
|
||||
@@ -527,7 +565,7 @@ roles | {grafana}
|
||||
cmd | ALL
|
||||
qual | ((owner_email)::text = current_setting('user.email'::text, true))
|
||||
with_check | false
|
||||
-[ RECORD 32 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 36 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | auth
|
||||
tablename | accounts
|
||||
policyname | api_user_role
|
||||
@@ -536,7 +574,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 33 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 37 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | auth
|
||||
tablename | accounts
|
||||
policyname | api_scheduler_role
|
||||
@@ -545,7 +583,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 34 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 38 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | auth
|
||||
tablename | accounts
|
||||
policyname | grafana_proxy_role
|
||||
@@ -554,7 +592,7 @@ roles | {grafana_auth}
|
||||
cmd | ALL
|
||||
qual | true
|
||||
with_check | false
|
||||
-[ RECORD 35 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 39 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | public
|
||||
tablename | process_queue
|
||||
policyname | admin_all
|
||||
@@ -563,7 +601,7 @@ roles | {username}
|
||||
cmd | ALL
|
||||
qual | true
|
||||
with_check | true
|
||||
-[ RECORD 36 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 40 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | public
|
||||
tablename | process_queue
|
||||
policyname | api_vessel_role
|
||||
@@ -572,7 +610,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 37 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 41 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | public
|
||||
tablename | process_queue
|
||||
policyname | api_user_role
|
||||
@@ -581,7 +619,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 38 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 42 ]-----------------------------------------------------------------------------------------------------------------------------
|
||||
schemaname | public
|
||||
tablename | process_queue
|
||||
policyname | api_scheduler_role
|
||||
@@ -596,13 +634,23 @@ Test nominatim reverse_geocode_py_fn
|
||||
reverse_geocode_py_fn | {"name": "Spain", "country_code": "es"}
|
||||
|
||||
Test geoip reverse_geoip_py_fn
|
||||
Test opverpass API overpass_py_fn
|
||||
-[ RECORD 1 ]--+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
overpass_py_fn | {"fee": "yes", "vhf": "09", "name": "Port Olímpic", "phone": "+34 933561016", "leisure": "marina", "website": "https://portolimpic.barcelona/", "wikidata": "Q171204", "wikipedia": "ca:Port Olímpic de Barcelona", "addr:street": "Moll de Xaloc", "power_supply": "yes", "seamark:type": "harbour", "addr:postcode": "08005", "internet_access": "wlan", "wikimedia_commons": "Category:Port Olímpic (Barcelona)", "sanitary_dump_station": "yes", "seamark:harbour:category": "marina"}
|
||||
|
||||
-[ 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"}
|
||||
|
||||
-[ RECORD 1 ]--+----------------------------------------------
|
||||
overpass_py_fn | {"name": "Norra hamnen", "leisure": "marina"}
|
||||
|
||||
-[ RECORD 1 ]----------------------------------------------------------------------------------------------------------------------------------------------
|
||||
versions_fn | {"api_version" : "0.3.0", "sys_version" : "PostgreSQL 15.4", "timescaledb" : "2.12.0", "postgis" : "3.4.0", "postgrest" : "PostgREST 11.2.1"}
|
||||
versions_fn | {"api_version" : "0.6.1", "sys_version" : "PostgreSQL 16.1", "timescaledb" : "2.13.1", "postgis" : "3.4.1", "postgrest" : "PostgREST 12.0.2"}
|
||||
|
||||
-[ RECORD 1 ]-----------------
|
||||
api_version | 0.3.0
|
||||
sys_version | PostgreSQL 15.4
|
||||
timescaledb | 2.12.0
|
||||
postgis | 3.4.0
|
||||
postgrest | PostgREST 11.2.1
|
||||
api_version | 0.6.1
|
||||
sys_version | PostgreSQL 16.1
|
||||
timescaledb | 2.13.1
|
||||
postgis | 3.4.1
|
||||
postgrest | PostgREST 12.0.2
|
||||
|
||||
|
@@ -36,7 +36,7 @@ SET vessel.name = 'kapla';
|
||||
--SET vessel.client_id = 'vessels.urn:mrn:imo:mmsi:123456789';
|
||||
--SELECT * FROM api.vessels_view v;
|
||||
SELECT name, mmsi, created_at IS NOT NULL as created_at, last_contact IS NOT NULL as last_contact FROM api.vessels_view v;
|
||||
SELECT name,geojson,watertemperature,insidetemperature,outsidetemperature FROM api.monitoring_view m;
|
||||
SELECT name,geojson->'geometry' as geometry,watertemperature,insidetemperature,outsidetemperature FROM api.monitoring_view m;
|
||||
|
||||
SET "user.email" = 'demo+aava@openplotter.cloud';
|
||||
SELECT set_config('vessel.id', :'vessel_id_aava', false) IS NOT NULL as vessel_id;
|
||||
@@ -45,4 +45,4 @@ SET vessel.name = 'aava';
|
||||
--SET vessel.client_id = 'vessels.urn:mrn:imo:mmsi:787654321';
|
||||
--SELECT * FROM api.vessels_view v;
|
||||
SELECT name, mmsi, created_at IS NOT NULL as created_at, last_contact IS NOT NULL as last_contact FROM api.vessels_view v;
|
||||
SELECT name,geojson,watertemperature,insidetemperature,outsidetemperature FROM api.monitoring_view m;
|
||||
SELECT name,geojson->'geometry' as geometry,watertemperature,insidetemperature,outsidetemperature FROM api.monitoring_view m;
|
||||
|
@@ -37,9 +37,9 @@ mmsi |
|
||||
created_at | t
|
||||
last_contact | t
|
||||
|
||||
-[ RECORD 1 ]------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 1 ]------+--------------------------------------------------------
|
||||
name | kapla
|
||||
geojson | {"type": "Feature", "geometry": {"type": "Point", "coordinates": [23.365766667, 59.86]}, "properties": {"name": "kapla", "latitude": 59.86, "longitude": 23.365766666666666}}
|
||||
geometry | {"type": "Point", "coordinates": [23.365766667, 59.86]}
|
||||
watertemperature |
|
||||
insidetemperature |
|
||||
outsidetemperature |
|
||||
@@ -55,9 +55,9 @@ mmsi | 787654321
|
||||
created_at | t
|
||||
last_contact | t
|
||||
|
||||
-[ RECORD 1 ]------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
-[ RECORD 1 ]------+------------------------------------------------------------
|
||||
name | aava
|
||||
geojson | {"type": "Feature", "geometry": {"type": "Point", "coordinates": [2.2934791, 41.465333283]}, "properties": {"name": "aava", "latitude": 41.46533328333334, "longitude": 2.2934791}}
|
||||
geometry | {"type": "Point", "coordinates": [2.2934791, 41.465333283]}
|
||||
watertemperature | 280.25
|
||||
insidetemperature |
|
||||
outsidetemperature |
|
||||
|
@@ -9,8 +9,25 @@ if [[ -z "${PGSAIL_API_URI}" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
#npm install
|
||||
npm install -g pnpm && pnpm install
|
||||
# psql
|
||||
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 install github.com/KarnerTh/mermerd@latest
|
||||
fi
|
||||
|
||||
# pnpm install
|
||||
if [[ ! -x "/usr/local/bin/pnpm" ]]; then
|
||||
npm install -g pnpm
|
||||
fi
|
||||
pnpm install || exit 1
|
||||
|
||||
# settings
|
||||
export mymocha="./node_modules/mocha/bin/_mocha"
|
||||
mkdir -p output/ && rm -rf output/*
|
||||
@@ -121,6 +138,7 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Monitoring API unit tests
|
||||
$mymocha index4.js --reporter ./node_modules/mochawesome --reporter-options reportDir=output/,reportFilename=report4.html
|
||||
if [ $? -eq 0 ]; then
|
||||
echo OK
|
||||
@@ -129,15 +147,60 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Monitoring unit tests
|
||||
# Monitoring SQL unit tests
|
||||
psql ${PGSAIL_DB_URI} < sql/monitoring.sql > output/monitoring.sql.output
|
||||
diff sql/monitoring.sql.output output/monitoring.sql.output > /dev/null
|
||||
#diff -u sql/monitoring.sql.output output/monitoring.sql.output | wc -l
|
||||
#echo 0
|
||||
if [ $? -eq 0 ]; then
|
||||
echo OK
|
||||
echo SQL monitoring.sql OK
|
||||
else
|
||||
echo SQL monitoring.sql FAILED
|
||||
diff -u sql/monitoring.sql.output output/monitoring.sql.output
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Anonymous API unit tests
|
||||
$mymocha index5.js --reporter ./node_modules/mochawesome --reporter-options reportDir=output/,reportFilename=report5.html
|
||||
if [ $? -eq 0 ]; then
|
||||
echo OK
|
||||
else
|
||||
echo mocha index5.js
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Anonymous SQL unit tests
|
||||
psql ${PGSAIL_DB_URI} < sql/anonymous.sql > output/anonymous.sql.output
|
||||
diff sql/anonymous.sql.output output/anonymous.sql.output > /dev/null
|
||||
#diff -u sql/anonymous.sql.output output/anonymous.sql.output | wc -l
|
||||
#echo 0
|
||||
if [ $? -eq 0 ]; then
|
||||
echo SQL anonymous.sql OK
|
||||
else
|
||||
echo SQL anonymous.sql FAILED
|
||||
diff -u sql/anonymous.sql.output output/anonymous.sql.output
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Download and update openapi documentation
|
||||
wget ${PGSAIL_API_URI} -O openapi.json
|
||||
#echo 0
|
||||
if [ $? -eq 0 ]; then
|
||||
cp openapi.json ../openapi.json
|
||||
echo openapi.json OK
|
||||
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
|
||||
if [ $? -eq 0 ]; then
|
||||
cp postgsail.md ../docs/ERD/postgsail.md
|
||||
echo postgsail.md OK
|
||||
else
|
||||
echo postgsail.md FAILED
|
||||
exit 1
|
||||
fi
|
||||
|