Compare commits
156 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ae57191cfb | ||
![]() |
9bdc777010 | ||
![]() |
49bad13fe7 | ||
![]() |
d465d91a94 | ||
![]() |
2edff87269 | ||
![]() |
e6ce0582d3 | ||
![]() |
31849a86b1 | ||
![]() |
de33977c83 | ||
![]() |
d0ace87fd7 | ||
![]() |
a7c6254f5f | ||
![]() |
4ab69d40ef | ||
![]() |
2c62c7b92c | ||
![]() |
75cf68dc78 | ||
![]() |
febc7f3a60 | ||
![]() |
788f609f3b | ||
![]() |
3aa26685eb | ||
![]() |
8ce04ec282 | ||
![]() |
21cc07f6c0 | ||
![]() |
dea8452229 | ||
![]() |
6b0eb72b82 | ||
![]() |
94960ad391 | ||
![]() |
577da72451 | ||
![]() |
ea2f3ec6d1 | ||
![]() |
2eb645123b | ||
![]() |
197e080035 | ||
![]() |
b6b082dd8c | ||
![]() |
fd97b4c616 | ||
![]() |
582cd4460e | ||
![]() |
c056737e2f | ||
![]() |
0ffe646050 | ||
![]() |
dfdc54062d | ||
![]() |
4a0f4c77ca | ||
![]() |
8038a95b60 | ||
![]() |
328cbc2741 | ||
![]() |
482510121c | ||
![]() |
f0929fd633 | ||
![]() |
9483560a18 | ||
![]() |
0f7284b8c8 | ||
![]() |
3a2ef95e25 | ||
![]() |
9bc463c45e | ||
![]() |
5bcb51f803 | ||
![]() |
c145d1c1df | ||
![]() |
87d9380882 | ||
![]() |
40256a1c0e | ||
![]() |
2ffbbbe885 | ||
![]() |
ef89437660 | ||
![]() |
f01a4b9605 | ||
![]() |
8f4a8c14ee | ||
![]() |
c978df1edb | ||
![]() |
3525b88bc2 | ||
![]() |
47249b90fe | ||
![]() |
e06db937e5 | ||
![]() |
cca75d252a | ||
![]() |
5144050875 | ||
![]() |
931544663e | ||
![]() |
1c62aaa853 | ||
![]() |
f1903ba3eb | ||
![]() |
ab5becb31d | ||
![]() |
44b034873e | ||
![]() |
e398eb2a99 | ||
![]() |
57ff0b97ea | ||
![]() |
a633731ae7 | ||
![]() |
b34162f11b | ||
![]() |
a5d1495864 | ||
![]() |
896576d0f8 | ||
![]() |
e3309d9784 | ||
![]() |
861fbf5502 | ||
![]() |
3f84a731b2 | ||
![]() |
c66797fa4f | ||
![]() |
d56d5d54a8 | ||
![]() |
f86a1b4382 | ||
![]() |
51b6e8fa7c | ||
![]() |
89af44efcc | ||
![]() |
a64ef5850d | ||
![]() |
da100ddd18 | ||
![]() |
92ce0503dd | ||
![]() |
61d40fd7b6 | ||
![]() |
c318f2d338 | ||
![]() |
c7c14fa5a1 | ||
![]() |
4fc68ae805 | ||
![]() |
3eb67abedb | ||
![]() |
894dbf0667 | ||
![]() |
f526b99853 | ||
![]() |
a670038f28 | ||
![]() |
2599f40f7b | ||
![]() |
4d833999e8 | ||
![]() |
b4dc93ba0e | ||
![]() |
764a6d6457 | ||
![]() |
2e9ede6da2 | ||
![]() |
cc67a3b37d | ||
![]() |
64ecbfc698 | ||
![]() |
b19eeed59a | ||
![]() |
8f5cd4237d | ||
![]() |
7b3a1451bb | ||
![]() |
a2cdd8ddfe | ||
![]() |
7a04026e67 | ||
![]() |
fab496ea3d | ||
![]() |
4f31831c94 | ||
![]() |
300e4bee48 | ||
![]() |
99e258c974 | ||
![]() |
970c85c11e | ||
![]() |
bbf4426f55 | ||
![]() |
a8620f4b4c | ||
![]() |
15accaa4cb | ||
![]() |
8d382b48ac | ||
![]() |
2983f149ad | ||
![]() |
a1ca97b549 | ||
![]() |
119c1778e6 | ||
![]() |
11489ce4aa | ||
![]() |
42b070baa8 | ||
![]() |
a1df7b218c | ||
![]() |
160d6aa569 | ||
![]() |
a2903e08ac | ||
![]() |
5a74914eac | ||
![]() |
55dc6275ee | ||
![]() |
f2c68c82d8 | ||
![]() |
578ca925db | ||
![]() |
ae14017cfc | ||
![]() |
1b42e3849f | ||
![]() |
2ffcbc5586 | ||
![]() |
235506f2bc | ||
![]() |
5a2ba54b2a | ||
![]() |
122c44c338 | ||
![]() |
2e451fa93c | ||
![]() |
d26d008b47 | ||
![]() |
6a6239f344 | ||
![]() |
2f6a0a6133 | ||
![]() |
bda652b87e | ||
![]() |
2f6bb6d5d9 | ||
![]() |
2cd9b0dd6c | ||
![]() |
13e4f453d5 | ||
![]() |
bc7d51c71e | ||
![]() |
95d3c5bded | ||
![]() |
f0c6f92920 | ||
![]() |
852d2ff583 | ||
![]() |
7cf7905694 | ||
![]() |
0f8107a672 | ||
![]() |
77dec463d1 | ||
![]() |
8ff1d0a8ed | ||
![]() |
859788d98d | ||
![]() |
62642ffbd6 | ||
![]() |
c3760c8689 | ||
![]() |
763c9ae802 | ||
![]() |
37abb3ae1f | ||
![]() |
a6da3cab0a | ||
![]() |
22f756b3a9 | ||
![]() |
cb3e9d8e57 | ||
![]() |
1997fe5a81 | ||
![]() |
5a1451ff69 | ||
![]() |
a18abec1f1 | ||
![]() |
322c3ed4fb | ||
![]() |
d648d119cc | ||
![]() |
9109474e8a | ||
![]() |
ca92a15eba | ||
![]() |
d745048a9c | ||
![]() |
6a0c15d23c |
114
.codesandbox/tasks.json
Normal file
@@ -0,0 +1,114 @@
|
||||
{
|
||||
// These tasks will run in order when initializing your CodeSandbox project.
|
||||
"setupTasks": [
|
||||
{
|
||||
"name": "git udpate",
|
||||
"command": "cd ~/workspace/ && git pull"
|
||||
},
|
||||
{
|
||||
"name": "git udpate submodule",
|
||||
"command": "cd ~/workspace/ && git submodule update --recursive --remote"
|
||||
}
|
||||
],
|
||||
|
||||
// These tasks can be run from CodeSandbox. Running one will open a log in the app.
|
||||
"tasks": {
|
||||
"docker-compose up db": {
|
||||
"name": "docker-compose up db",
|
||||
"command": "docker-compose up db",
|
||||
"runAtStart": true
|
||||
},
|
||||
"docker network inspect network": {
|
||||
"name": "docker network inspect postgsail_default",
|
||||
"command": "docker network ls && docker network inspect postgsail_default",
|
||||
"runAtStart": false
|
||||
},
|
||||
"docker-compose up api": {
|
||||
"name": "docker-compose up api",
|
||||
"command": "docker-compose up api",
|
||||
"runAtStart": false,
|
||||
"preview": {
|
||||
"port": 3000,
|
||||
"prLink": "direct"
|
||||
}
|
||||
},
|
||||
"docker volume rm volume": {
|
||||
"name": "docker volume rm volume",
|
||||
"command": "docker volume ls && docker volume rm postgsail_data",
|
||||
"runAtStart": false
|
||||
},
|
||||
"docker-compose rm db": {
|
||||
"name": "docker-compose rm db",
|
||||
"command": "docker-compose rm db",
|
||||
"runAtStart": false
|
||||
},
|
||||
"docker-compose rm api": {
|
||||
"name": "docker-compose rm api",
|
||||
"command": "docker-compose rm api",
|
||||
"runAtStart": false
|
||||
},
|
||||
"docker-compose clean": {
|
||||
"name": "docker-compose clean",
|
||||
"command": "docker-compose stop && docker-compose rm && docker volume ls && docker volume rm postgsail_data",
|
||||
"runAtStart": false
|
||||
},
|
||||
"docker-compose pgadmin": {
|
||||
"name": "docker-compose up pgadmin",
|
||||
"command": "docker-compose up pgadmin",
|
||||
"runAtStart": false,
|
||||
"preview": {
|
||||
"port": 5050,
|
||||
"prLink": "direct"
|
||||
}
|
||||
},
|
||||
"docker-compose web": {
|
||||
"name": "docker-compose up web",
|
||||
"command": "docker-compose up web",
|
||||
"runAtStart": false,
|
||||
"preview": {
|
||||
"port": 8080,
|
||||
"prLink": "direct"
|
||||
}
|
||||
},
|
||||
"docker-compose ps": {
|
||||
"name": "docker-compose ps -a",
|
||||
"command": "docker-compose ps -a",
|
||||
"runAtStart": false
|
||||
},
|
||||
"docker ps": {
|
||||
"name": "docker ps -a",
|
||||
"command": "docker ps -a",
|
||||
"runAtStart": false
|
||||
},
|
||||
"docker-compose stop": {
|
||||
"name": "docker-compose stop",
|
||||
"command": "docker-compose stop",
|
||||
"runAtStart": false
|
||||
},
|
||||
"npm i": {
|
||||
"name": "npm i",
|
||||
"command": "cd frontend/ && npm i",
|
||||
"runAtStart": false
|
||||
},
|
||||
"git submodule add https://github.com/xbgmsharp/vuestic-postgsail frontend": {
|
||||
"name": "git submodule add https://github.com/xbgmsharp/vuestic-postgsail frontend",
|
||||
"command": "git submodule add https://github.com/xbgmsharp/vuestic-postgsail frontend",
|
||||
"runAtStart": false
|
||||
},
|
||||
"git submodule update --init --recursive": {
|
||||
"name": "git submodule update --init --recursive",
|
||||
"command": "git submodule update --init --recursive",
|
||||
"runAtStart": false
|
||||
},
|
||||
"git submodule update --recursive --remote": {
|
||||
"name": "git submodule update --recursive --remote",
|
||||
"command": "git submodule update --recursive --remote",
|
||||
"runAtStart": false
|
||||
},
|
||||
"git pull": {
|
||||
"name": "git pull",
|
||||
"command": "git pull",
|
||||
"runAtStart": false
|
||||
}
|
||||
}
|
||||
}
|
76
.devcontainer.json
Normal file
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"name": "PostgSail",
|
||||
//"image": "mcr.microsoft.com/devcontainers/base:alpine",
|
||||
"dockerComposeFile": ["docker-compose.dev.yml", "docker-compose.yml"],
|
||||
"service": "dev",
|
||||
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
|
||||
|
||||
// Use this environment variable if you need to bind mount your local source code into a new container.
|
||||
"remoteEnv": {
|
||||
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}",
|
||||
"POSTGRES_PASSWORD": "${localEnv:POSTGRES_PASSWORD}",
|
||||
"POSTGRES_USER": "${localEnv:POSTGRES_USER}",
|
||||
"POSTGRES_DB": "${localEnv:POSTGRES_DB}",
|
||||
"PGSAIL_AUTHENTICATOR_PASSWORD": "${localEnv:PGSAIL_AUTHENTICATOR_PASSWORD}"
|
||||
},
|
||||
"containerEnv": {
|
||||
//"GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}",
|
||||
//"GITHUB_USER": "${localEnv:GITHUB_USER}"
|
||||
},
|
||||
|
||||
// Features to add to the dev container. More info: https://containers.dev/features.
|
||||
// "features": {},
|
||||
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
"forwardPorts": ["db:5432", "api:3000", "pgadmin:5050", "web:8080"],
|
||||
|
||||
// Use 'portsAttributes' to set default properties for specific forwarded ports.
|
||||
// More info: https://containers.dev/implementors/json_reference/#port-attributes
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "api",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"5050": {
|
||||
"label": "pgadmin",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"5342": {
|
||||
"label": "database",
|
||||
"onAutoForward": "notify"
|
||||
},
|
||||
"8080": {
|
||||
"label": "web",
|
||||
"onAutoForward": "notify"
|
||||
}
|
||||
},
|
||||
|
||||
// Use 'postCreateCommand' to run commands after the container is created.
|
||||
// "postCreateCommand": "docker --version",
|
||||
|
||||
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
|
||||
// "remoteUser": "root"
|
||||
|
||||
// Configure tool-specific properties.
|
||||
"customizations": {
|
||||
// Configure properties specific to VS Code.
|
||||
"vscode": {
|
||||
"settings": {
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"zsh": {
|
||||
"path": "/bin/bash"
|
||||
}
|
||||
},
|
||||
"terminal.integrated.defaultProfile.linux": "bash",
|
||||
"editor.formatOnSave": true
|
||||
},
|
||||
"extensions": [
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"esbenp.prettier-vscode",
|
||||
"ckolkman.vscode-postgres",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
@@ -12,10 +12,11 @@ PGSAIL_EMAIL_SERVER=localhost
|
||||
#PGSAIL_EMAIL_PASS= Comment if not use
|
||||
#PGSAIL_PUSHOVER_APP_TOKEN= Comment if not use
|
||||
#PGSAIL_PUSHOVER_APP_URL= Comment if not use
|
||||
#PGSAIL_PGSAIL_TELEGRAM_BOT_TOKEN= Comment if not use
|
||||
PGSAIL_APP_URL=http://localhost
|
||||
#PGSAIL_TELEGRAM_BOT_TOKEN= Comment if not use
|
||||
PGSAIL_APP_URL=http://localhost:8080
|
||||
PGSAIL_API_URL=http://localhost:3000
|
||||
# POSTGREST ENV Settings
|
||||
PGRST_DB_URI=postgres://authenticator:${PGSAIL_AUTHENTICATOR_PASSWORD}@127.0.0.1:5432/signalk
|
||||
PGRST_DB_URI=postgres://authenticator:${PGSAIL_AUTHENTICATOR_PASSWORD}@db:5432/signalk
|
||||
PGRST_JWT_SECRET=_at_least_32__char__long__random
|
||||
# Grafana ENV Settings
|
||||
GF_SECURITY_ADMIN_PASSWORD=password
|
||||
|
61
.github/workflows/db-test.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
name: Test services db, api
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'initdb/**'
|
||||
branches:
|
||||
- 'main'
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'initdb/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
smoketest:
|
||||
name: tests
|
||||
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: Install psql
|
||||
run: sudo apt install postgresql-client
|
||||
|
||||
- name: Run PostgSail Database & API tests
|
||||
# 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 15 && docker-compose up -d api && sleep 5
|
||||
docker-compose ps -a
|
||||
echo ${PGSAIL_API_URL}
|
||||
curl ${PGSAIL_API_URL}
|
||||
psql -c "select 1"
|
||||
echo "Test PostgreSQL version"
|
||||
psql -c "SELECT version();"
|
||||
echo "Test PostgSail version"
|
||||
psql -c "SELECT value FROM app_settings WHERE name = 'app.version';"
|
||||
echo "Test PostgSail Unit Test"
|
||||
docker-compose -f docker-compose.dev.yml -f docker-compose.yml up tests
|
||||
- name: Show the logs
|
||||
if: always()
|
||||
run: |
|
||||
docker-compose logs
|
52
.github/workflows/frontend-test.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Test services db, api, web
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
branches:
|
||||
- 'main'
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'frontend/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ci-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Pull Docker images
|
||||
run: docker-compose pull db api web
|
||||
|
||||
- name: Run PostgSail Web test
|
||||
# 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 15 && docker-compose up -d api && sleep 5
|
||||
docker-compose ps -a
|
||||
echo "Test PostgSail Web Unit Test"
|
||||
docker-compose up -d web && sleep 5
|
||||
docker-compose ps -a
|
||||
curl http://localhost:8080/
|
||||
- name: Show the logs
|
||||
if: always()
|
||||
run: |
|
||||
docker-compose logs
|
52
.github/workflows/grafana-test.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Test services db, grafana
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'grafana/**'
|
||||
branches:
|
||||
- 'main'
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'grafana/**'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ci-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set env
|
||||
run: cp .env.example .env
|
||||
|
||||
- name: Pull Docker images
|
||||
run: docker-compose pull db app
|
||||
|
||||
- name: Run PostgSail Grafana test
|
||||
# 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 15
|
||||
docker-compose ps -a
|
||||
echo "Test PostgSail Grafana Unit Test"
|
||||
docker-compose up -d app && sleep 5
|
||||
docker-compose ps -a
|
||||
curl http://localhost:3001/
|
||||
- name: Show the logs
|
||||
if: always()
|
||||
run: |
|
||||
docker-compose logs
|
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
[submodule "frontend"]
|
||||
path = frontend
|
||||
url = https://github.com/xbgmsharp/vuestic-postgsail
|
Before Width: | Height: | Size: 104 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 30 KiB |
@@ -13,7 +13,7 @@ There is 3 main schemas:
|
||||
- ...
|
||||
- functions
|
||||
- ...
|
||||

|
||||

|
||||
|
||||
- Auth Schema ERD
|
||||
- tables
|
||||
@@ -22,7 +22,7 @@ There is 3 main schemas:
|
||||
- ...
|
||||
- functions
|
||||
- ...
|
||||

|
||||

|
||||
|
||||
- Public Schema ERD
|
||||
- tables
|
||||
@@ -31,5 +31,5 @@ There is 3 main schemas:
|
||||
- ...
|
||||
- functions
|
||||
- ...
|
||||

|
||||

|
||||
|
||||
|
BIN
ERD/signalk - api.png
Normal file
After Width: | Height: | Size: 222 KiB |
BIN
ERD/signalk - auth.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
ERD/signalk - public.png
Normal file
After Width: | Height: | Size: 194 KiB |
BIN
PostgSail.png
Normal file
After Width: | Height: | Size: 97 KiB |
80
README.md
@@ -1,7 +1,9 @@
|
||||
# PostgSail
|
||||
|
||||
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.
|
||||
|
||||
## 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!
|
||||
@@ -14,43 +16,91 @@ Effortless cloud based solution for storing and sharing your SignalK data. Allow
|
||||
- Alert monitoring: get notification on low voltage or low fuel remotely.
|
||||
- Notification via email or PushOver, Telegram
|
||||
- Offline mode
|
||||
- Low Bandwith mode
|
||||
- Low Bandwidth mode
|
||||
|
||||
## Context
|
||||
|
||||
It is all about SQL, object-relational, time-series, spatial databases with a bit of python.
|
||||
|
||||
PostgSail is an open-source alternative to traditional vessel data management.
|
||||
It is based on a well known open-source technology stack, Singalk, PostgreSQL, TimescaleDB, PostGIS, PostgREST. It does perfectly integrate with standard monitoring tool stack like Grafana.
|
||||
It is based on a well known open-source technology stack, Signalk, PostgreSQL, TimescaleDB, PostGIS, PostgREST. It does perfectly integrate with standard monitoring tool stack like Grafana.
|
||||
|
||||
To understand the why and how, you might want to read [Why.md](https://github.com/xbgmsharp/postgsail/tree/main/Why.md)
|
||||
|
||||
## Architecture
|
||||
A simple scalable architecture:
|
||||
|
||||

|
||||
|
||||
For more clarity and visibility the complete [Entity-Relationship Diagram (ERD)](https://github.com/xbgmsharp/postgsail/tree/main/ERD/README.md) is export as PNG and SVG file.
|
||||
|
||||
### Cloud
|
||||
## Cloud
|
||||
|
||||
If you prefer not to install or administer your instance of PostgSail, hosted versions of PostgSail are available in the cloud of your choice.
|
||||
|
||||
The cloud advantage.
|
||||
### The cloud advantage.
|
||||
|
||||
Hosted and fully–managed options for PostgSail, designed for all your deployment and business needs. Register and try for free at https://iot.openplotter.cloud/.
|
||||
|
||||
## Using PostgSail
|
||||
|
||||
### full-featured development environment
|
||||
The Visual Studio Code Remote - Containers extension lets you use a Docker container as a full-featured development environment.
|
||||
|
||||
#### With codesandbox
|
||||
- https://codesandbox.io/p/github/xbgmsharp/postgsail/main
|
||||
|
||||
#### With DevPod
|
||||
- https://devpod.sh/open#https://github.com/xbgmsharp/postgsail/&workspace=postgsail&provider=docker&ide=openvscode
|
||||
|
||||
#### With Docker Dev Environments
|
||||
- https://open.docker.com/dashboard/dev-envs?url=https://github.com/xbgmsharp/postgsail/
|
||||
Open in Docker Dev Environments Open in Docker Dev Environments
|
||||
|
||||
### pre-deploy configuration
|
||||
|
||||
To get these running, copy `.env.example` and rename to `.env` then set the value accordinly.
|
||||
To get these running, copy `.env.example` and rename to `.env` then set the value accordingly.
|
||||
|
||||
```bash
|
||||
# cp .env.example .env
|
||||
```
|
||||
|
||||
Notice, that `PGRST_JWT_SECRET` must be at least 32 characters long.
|
||||
|
||||
`$ head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 ; echo ''`
|
||||
`$ head /dev/urandom | tr -dc A-Za-z0-9 | head -c 42 ; echo ''`
|
||||
|
||||
```bash
|
||||
# nano .env
|
||||
```
|
||||
|
||||
### Deploy
|
||||
|
||||
By default there is no network set and the postgresql data are store in a docker volume.
|
||||
You can update the default settings by editing `docker-compose.yml` to your need.
|
||||
Then simply excecute:
|
||||
|
||||
First let's initialize the database.
|
||||
|
||||
#### Initialize database
|
||||
|
||||
First let's import the SQL schema, execute:
|
||||
|
||||
```bash
|
||||
$ docker-compose up
|
||||
$ docker-compose up db
|
||||
```
|
||||
|
||||
#### Start backend (db, api)
|
||||
|
||||
Then launch the full stack (db, api) backend, execute:
|
||||
|
||||
```bash
|
||||
$ docker-compose up db api
|
||||
```
|
||||
|
||||
The API should be accessible via port HTTP/3000.
|
||||
The database should be accessible via port TCP/5432.
|
||||
|
||||
You can connect to the database via a web gui like [pgadmin](https://www.pgadmin.org/) or you can use a client [dbeaver](https://dbeaver.io/).
|
||||
|
||||
### SQL Configuration
|
||||
|
||||
Check and update your postgsail settings via SQL in the table `app_settings`:
|
||||
@@ -67,33 +117,39 @@ UPDATE app_settings
|
||||
```
|
||||
|
||||
### Ingest data
|
||||
|
||||
Next, to ingest data from signalk, you need to install [signalk-postgsail](https://github.com/xbgmsharp/signalk-postgsail) plugin on your signalk server instance.
|
||||
|
||||
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 definitly possible.
|
||||
Any taker on influxdb2 to PostgSail? It is definitely possible.
|
||||
|
||||
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 sucessfully and you should recieve a few notifications by email or PushOver.
|
||||
If everything goes well all tests pass successfully and you should receive a few notifications by email or PushOver.
|
||||
|
||||
```
|
||||
$ docker-compose up tests
|
||||
```
|
||||
|
||||
### API Documentation
|
||||
|
||||
The OpenAPI description output depends on the permissions of the role that is contained in the JWT role claim.
|
||||
|
||||
API anonymous:
|
||||
|
||||
```
|
||||
$ curl http://localhost:3000/
|
||||
```
|
||||
|
||||
API user_role:
|
||||
|
||||
```
|
||||
$ curl http://localhost:3000/ -H 'Authorization: Bearer my_token_from_login_or_signup_fn'
|
||||
```
|
||||
|
||||
API vessel_role:
|
||||
|
||||
```
|
||||
$ curl http://localhost:3000/ -H 'Authorization: Bearer my_token_from_register_vessel_fn'
|
||||
```
|
||||
@@ -114,12 +170,15 @@ Check the [unit test sample](https://github.com/xbgmsharp/postgsail/blob/main/te
|
||||
|
||||
- [pgAdmin](https://hub.docker.com/r/dpage/pgadmin4), web UI to monitor and manage multiple PostgreSQL
|
||||
- [Swagger](https://hub.docker.com/r/swaggerapi/swagger-ui), web UI to visualize documentation from PostgREST
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose-optional.yml up
|
||||
```
|
||||
|
||||
### Software reference
|
||||
|
||||
Out of the box iot platform using docker with the following software:
|
||||
|
||||
- [Signal K server, a Free and Open Source universal marine data exchange format](https://signalk.org)
|
||||
- [PostgreSQL, open source object-relational database system](https://postgresql.org)
|
||||
- [TimescaleDB, Time-series data extends PostgreSQL](https://www.timescale.com)
|
||||
@@ -127,6 +186,7 @@ Out of the box iot platform using docker with the following software:
|
||||
- [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
|
||||
|
15
Why.md
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
#### Why not InfluxDB vs TimescaleDB
|
||||
I had an InfluxDBv1 on my RPI that kill the sdcard/usbkey. I had an InfluxDBv2, but there is no more ARM support and had to learn flux. Also could not find a good way to store data when offline. How do you export your data from a InfluxDBv2? Still looking for a solution.
|
||||
|
||||
With TimescaleDB, we already know SQL and there is a lot of tools and libraries that work with Postgres.
|
||||
However, InfluxDB does simplify things like schema and provide an http endpoint.
|
||||
With TimescaleDB, you are using a standard SQL table schema to store data from Signalk.
|
||||
|
||||
#### Why not MQTT vs HTTP
|
||||
Having MQTT, makes your application micro service approach. however you multiple the components and dependency. HTTP seem a more reliable solution specially for offline support as MQTT library have a buffer limitation.
|
||||
Using PostgREST is an alternative to manual CRUD programming. Custom API servers suffer problems. Writing business logic often duplicates, ignores or hobbles database structure. Object-relational mapping is a leaky abstraction leading to slow imperative code. The PostgREST philosophy establishes a single declarative source of truth: the data itself.
|
||||
|
||||
#### PostgreSQL got it all!
|
||||
No additional dependencies other than PostgreSQL, thanks to the extensions ecosystem.
|
||||
With PostgSail is based on PostGis and TimescaleDB and a few other pg extensions, https://github.com/xbgmsharp/timescaledb-postgis, fore more details.
|
113
docker-compose.dev.yml
Normal file
@@ -0,0 +1,113 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
dev:
|
||||
container_name: dev
|
||||
image: mcr.microsoft.com/devcontainers/base:ubuntu
|
||||
volumes:
|
||||
- ../:/workspaces:cached
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
#network_mode: service:db
|
||||
links:
|
||||
- "api:postgrest"
|
||||
- "db:database"
|
||||
#- "web_dev:web_dev"
|
||||
command: sleep infinity
|
||||
|
||||
pgadmin:
|
||||
image: dpage/pgadmin4:latest
|
||||
container_name: pgadmin
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- data:/var/lib/pgadmin
|
||||
- ./pgadmin_servers.json:/servers.json:ro
|
||||
links:
|
||||
- "db:database"
|
||||
ports:
|
||||
- 5050:5050
|
||||
environment:
|
||||
- PGADMIN_DEFAULT_EMAIL=${PGADMIN_DEFAULT_EMAIL}
|
||||
- PGADMIN_DEFAULT_PASSWORD=${PGADMIN_DEFAULT_PASSWORD}
|
||||
- PGADMIN_LISTEN_ADDRESS=0.0.0.0
|
||||
- PGADMIN_LISTEN_PORT=5050
|
||||
- PGADMIN_SERVER_JSON_FILE=/servers.json
|
||||
- PGADMIN_DISABLE_POSTFIX=true
|
||||
depends_on:
|
||||
- db
|
||||
logging:
|
||||
options:
|
||||
max-size: 10m
|
||||
|
||||
swagger:
|
||||
image: swaggerapi/swagger-ui
|
||||
container_name: swagger
|
||||
restart: unless-stopped
|
||||
links:
|
||||
- "api:postgrest"
|
||||
ports:
|
||||
- "8181:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
environment:
|
||||
- API_URL=http://api:3000/
|
||||
depends_on:
|
||||
- db
|
||||
- api
|
||||
logging:
|
||||
options:
|
||||
max-size: 10m
|
||||
|
||||
tests:
|
||||
image: xbgmsharp/postgsail-tests
|
||||
build:
|
||||
context: ./tests
|
||||
dockerfile: Dockerfile
|
||||
container_name: tests
|
||||
volumes:
|
||||
- ./tests:/mnt
|
||||
working_dir: /mnt
|
||||
command: 'bash tests.sh'
|
||||
links:
|
||||
- "api:postgrest"
|
||||
- "db:database"
|
||||
env_file: .env
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- PGPASSWORD=${POSTGRES_PASSWORD}
|
||||
- PGSAIL_API_URI=http://api:3000
|
||||
- PGSAIL_DB_URI=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/signalk
|
||||
depends_on:
|
||||
- db
|
||||
- api
|
||||
logging:
|
||||
options:
|
||||
max-size: 10m
|
||||
|
||||
web_dev:
|
||||
image: xbgmsharp/postgsail-vuestic:dev
|
||||
build:
|
||||
context: https://github.com/xbgmsharp/vuestic-postgsail.git#live
|
||||
dockerfile: Dockerfile_dev
|
||||
container_name: web_dev
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./frontend:/app
|
||||
links:
|
||||
- "api:postgrest"
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
- VITE_PGSAIL_URL=${PGSAIL_API_URL}
|
||||
- VITE_APP_INCLUDE_DEMOS=false
|
||||
- VITE_APP_BUILD_VERSION=true
|
||||
- VITE_APP_TITLE=${VITE_APP_TITLE}
|
||||
depends_on:
|
||||
- db
|
||||
- api
|
||||
logging:
|
||||
options:
|
||||
max-size: 10m
|
||||
|
||||
volumes:
|
||||
data: {}
|
@@ -1,21 +1,25 @@
|
||||
version: '3.9'
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: xbgmsharp/timescaledb-postgis
|
||||
container_name: db
|
||||
hostname: db
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
- POSTGRES_DB=postgres
|
||||
- TIMESCALEDB_TELEMETRY=off
|
||||
- PGDATA=/var/lib/postgresql/data/pgdata
|
||||
- TZ=UTC
|
||||
network_mode: "host"
|
||||
- POSTGRES_USER=${POSTGRES_USER}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
|
||||
- POSTGRES_DB=${POSTGRES_DB}
|
||||
- PGSAIL_AUTHENTICATOR_PASSWORD=${PGSAIL_AUTHENTICATOR_PASSWORD}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- data:/var/lib/postgresql/data
|
||||
- $PWD/initdb:/docker-entrypoint-initdb.d
|
||||
- ./db-data:/var/lib/postgresql/data
|
||||
- ./initdb:/docker-entrypoint-initdb.d
|
||||
logging:
|
||||
options:
|
||||
max-size: 10m
|
||||
@@ -29,7 +33,10 @@ services:
|
||||
api:
|
||||
image: postgrest/postgrest
|
||||
container_name: api
|
||||
hostname: api
|
||||
restart: unless-stopped
|
||||
links:
|
||||
- "db:database"
|
||||
ports:
|
||||
- "3000:3000"
|
||||
env_file: .env
|
||||
@@ -38,7 +45,8 @@ 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
|
||||
network_mode: "host"
|
||||
PGRST_DB_URI: ${PGRST_DB_URI}
|
||||
PGRST_JWT_SECRET: ${PGRST_JWT_SECRET}
|
||||
depends_on:
|
||||
- db
|
||||
logging:
|
||||
@@ -55,18 +63,21 @@ services:
|
||||
image: grafana/grafana:latest
|
||||
container_name: app
|
||||
restart: unless-stopped
|
||||
links:
|
||||
- "db:database"
|
||||
volumes:
|
||||
- data:/var/lib/grafana
|
||||
- data:/var/log/grafana
|
||||
- $PWD/grafana:/etc/grafana
|
||||
- ./grafana:/etc/grafana
|
||||
ports:
|
||||
- "3001:3000"
|
||||
network_mode: "host"
|
||||
env_file: .env
|
||||
environment:
|
||||
- GF_INSTALL_PLUGINS=pr0ps-trackmap-panel,fatcloud-windrose-panel
|
||||
- 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:
|
||||
@@ -79,5 +90,43 @@ services:
|
||||
# retries: 5
|
||||
# start_period: 100s
|
||||
|
||||
telegram:
|
||||
image: xbgmsharp/postgsail-telegram-bot
|
||||
container_name: telegram
|
||||
restart: unless-stopped
|
||||
links:
|
||||
- "api:postgrest"
|
||||
ports:
|
||||
- "3005:8080"
|
||||
environment:
|
||||
- BOT_TOKEN=${PGSAIL_TELEGRAM_BOT_TOKEN}
|
||||
- PGSAIL_URL=${PGSAIL_API_URL}
|
||||
depends_on:
|
||||
- db
|
||||
- api
|
||||
logging:
|
||||
options:
|
||||
max-size: 10m
|
||||
|
||||
web:
|
||||
image: xbgmsharp/postgsail-vuestic
|
||||
container_name: web
|
||||
restart: unless-stopped
|
||||
links:
|
||||
- "api:postgrest"
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
- VITE_PGSAIL_URL=${PGSAIL_API_URL}
|
||||
- VITE_APP_INCLUDE_DEMOS=false
|
||||
- VITE_APP_BUILD_VERSION=true
|
||||
- VITE_APP_TITLE=${VITE_APP_TITLE}
|
||||
depends_on:
|
||||
- db
|
||||
- api
|
||||
logging:
|
||||
options:
|
||||
max-size: 10m
|
||||
|
||||
volumes:
|
||||
data: {}
|
||||
|
1
frontend
Submodule
1455
grafana/dashboards/Electrical.json
Normal file
@@ -54,7 +54,9 @@
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"displayMode": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"filterable": false,
|
||||
"inspect": false
|
||||
},
|
||||
@@ -109,6 +111,7 @@
|
||||
"id": 2,
|
||||
"options": {
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
@@ -118,7 +121,7 @@
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "9.3.1",
|
||||
"pluginVersion": "9.4.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -130,7 +133,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "with config as ( select set_config('vessel.id', '${boat}', false) )\nSELECT * from api.logs_view",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT * from api.logs_view",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -165,7 +168,7 @@
|
||||
"where": []
|
||||
}
|
||||
],
|
||||
"title": "Logbook",
|
||||
"title": "Logbook ${__user.email} / ${__user.login}",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
@@ -180,7 +183,9 @@
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"displayMode": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"filterable": false,
|
||||
"inspect": false
|
||||
},
|
||||
@@ -235,6 +240,7 @@
|
||||
"id": 5,
|
||||
"options": {
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
@@ -244,7 +250,7 @@
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "9.3.1",
|
||||
"pluginVersion": "9.4.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -256,7 +262,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "with config as ( select set_config('vessel.id', '${boat}', false) )\nSELECT * from api.stays_view",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT * from api.stays_view",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -306,7 +312,9 @@
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"displayMode": "auto",
|
||||
"cellOptions": {
|
||||
"type": "auto"
|
||||
},
|
||||
"filterable": false,
|
||||
"inspect": false
|
||||
},
|
||||
@@ -361,6 +369,7 @@
|
||||
"id": 6,
|
||||
"options": {
|
||||
"footer": {
|
||||
"countRows": false,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
@@ -370,7 +379,7 @@
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "9.3.1",
|
||||
"pluginVersion": "9.4.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -382,7 +391,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "with config as ( select set_config('vessel.id', '${boat}', false) )\nselect * from api.moorages_view",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nselect * from api.moorages_view",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -421,7 +430,9 @@
|
||||
"type": "table"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 37,
|
||||
"refresh": "",
|
||||
"revision": 1,
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
@@ -431,7 +442,7 @@
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"definition": "SELECT\n v.name AS __text,\n m.client_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;",
|
||||
"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,
|
||||
@@ -439,7 +450,7 @@
|
||||
"multi": false,
|
||||
"name": "boat",
|
||||
"options": [],
|
||||
"query": "SELECT\n v.name AS __text,\n m.client_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;",
|
||||
"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,
|
||||
|
@@ -92,7 +92,7 @@
|
||||
"text": {},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.3.1",
|
||||
"pluginVersion": "9.4.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -104,7 +104,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'electrical.batteries.AUX2.voltage' AS numeric) AS AUX2Voltage\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'electrical.batteries.AUX2.voltage' AS numeric) AS AUX2Voltage\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -198,7 +198,7 @@
|
||||
"text": {},
|
||||
"textMode": "auto"
|
||||
},
|
||||
"pluginVersion": "9.3.1",
|
||||
"pluginVersion": "9.4.3",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
@@ -210,7 +210,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS OutsideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS OutsideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -370,7 +370,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'electrical.batteries.AUX2.voltage' AS numeric) AS AUX2,\n\tcast(metrics-> 'electrical.batteries.House.voltage' AS numeric) AS House,\n\tcast(metrics-> 'environment.rpi.pijuice.gpioVoltage' AS numeric) AS gpioVoltage,\n\tcast(metrics-> 'electrical.batteries.Seatalk.voltage' AS numeric) AS SeatalkVoltage,\n\tcast(metrics-> 'electrical.batteries.Starter.voltage' AS numeric) AS StarterVoltage,\n\tcast(metrics-> 'environment.rpi.pijuice.batteryVoltage' AS numeric) AS RPIBatteryVoltage,\n\tcast(metrics-> 'electrical.batteries.victronDevice.voltage' AS numeric) AS victronDeviceVoltage\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n\tAND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'electrical.batteries.AUX2.voltage' AS numeric) AS AUX2,\n\tcast(metrics-> 'electrical.batteries.House.voltage' AS numeric) AS House,\n\tcast(metrics-> 'environment.rpi.pijuice.gpioVoltage' AS numeric) AS gpioVoltage,\n\tcast(metrics-> 'electrical.batteries.Seatalk.voltage' AS numeric) AS SeatalkVoltage,\n\tcast(metrics-> 'electrical.batteries.Starter.voltage' AS numeric) AS StarterVoltage,\n\tcast(metrics-> 'environment.rpi.pijuice.batteryVoltage' AS numeric) AS RPIBatteryVoltage,\n\tcast(metrics-> 'electrical.batteries.victronDevice.voltage' AS numeric) AS victronDeviceVoltage\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n\tAND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -505,7 +505,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'environment.water.temperature' AS numeric) - 273.15 AS waterTemperature,\n\tcast(metrics-> 'environment.inside.temperature' AS numeric) - 273.15 AS insideTemperature,\n\tcast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS outsideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.water.temperature' AS numeric) - 273.15 AS waterTemperature,\n\tcast(metrics-> 'environment.inside.temperature' AS numeric) - 273.15 AS insideTemperature,\n\tcast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS outsideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -638,7 +638,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "with config as (select set_config('vessel.id', '${boat}', false) ) select * from api.monitoring_view",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nwith config as (select set_config('vessel.id', '${boat}', false) ) select * from api.monitoring_view",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -683,8 +683,9 @@
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"schemaVersion": 37,
|
||||
"refresh": "",
|
||||
"revision": 1,
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
@@ -694,7 +695,7 @@
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"definition": " SELECT\n v.name AS __text,\n m.client_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;",
|
||||
"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,
|
||||
@@ -702,7 +703,7 @@
|
||||
"multi": false,
|
||||
"name": "boat",
|
||||
"options": [],
|
||||
"query": " SELECT\n v.name AS __text,\n m.client_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;",
|
||||
"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,
|
||||
@@ -712,7 +713,7 @@
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-12h",
|
||||
"from": "now-30d",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
|
1341
grafana/dashboards/RPI.json
Normal file
1987
grafana/dashboards/Solar.json
Normal file
@@ -118,7 +118,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(windspeedapparent AS numeric) * 1.9438444924406 AS windSpeed\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n and client_id = '${boat}'\nORDER BY 1\n",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(windspeedapparent AS numeric) * 1.9438444924406 AS windSpeed\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n and vessel_id = '${boat}'\nORDER BY 1\n",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -236,7 +236,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT cast(anglespeedapparent AS numeric) AS windAngleTrue from api.metrics WHERE client_id = '${boat}' ORDER BY time DESC LIMIT 1;",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT cast(anglespeedapparent AS numeric) AS windAngleTrue from api.metrics WHERE vessel_id = '${boat}' ORDER BY time DESC LIMIT 1;",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -363,7 +363,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS outsideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS outsideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -487,7 +487,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'environment.water.temperature' AS numeric) - 273.15 AS waterTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.water.temperature' AS numeric) - 273.15 AS waterTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -603,7 +603,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.pressure' AS numeric) * 0.00029530 AS outsidePressure\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1\n",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.pressure' AS numeric) * 0.00029530 AS outsidePressure\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1\n",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -726,7 +726,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'environment.inside.temperature' AS numeric) - 273.15 AS insideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.inside.temperature' AS numeric) - 273.15 AS insideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -852,7 +852,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.humidity' AS numeric) * 100 AS insideHumidity\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.humidity' AS numeric) * 100 AS insideHumidity\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -976,7 +976,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'environment.engine.temperature' AS numeric) - 273.15 AS insideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.engine.temperature' AS numeric) - 273.15 AS insideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -1134,7 +1134,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT time AS \"time\", cast(windspeedapparent AS numeric) * 1.9438444924406 AS windSpeed\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT time AS \"time\", cast(windspeedapparent AS numeric) * 1.9438444924406 AS windSpeed\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -1331,7 +1331,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'environment.water.temperature' AS numeric) - 273.15 AS waterTemperature,\n cast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS outsideTemperature,\n cast(metrics-> 'environment.inside.temperature' AS numeric) - 273.15 AS insideTemperature,\n cast(metrics-> 'environment.inside.fridge.temperature' AS numeric) - 273.15 AS fridgeTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.water.temperature' AS numeric) - 273.15 AS waterTemperature,\n cast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS outsideTemperature,\n cast(metrics-> 'environment.inside.temperature' AS numeric) - 273.15 AS insideTemperature,\n cast(metrics-> 'environment.inside.fridge.temperature' AS numeric) - 273.15 AS fridgeTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -1439,7 +1439,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS outsideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.temperature' AS numeric) - 273.15 AS outsideTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -1576,7 +1576,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.pressure' AS numeric) * 0.00029530 AS outsideTemperature,\n cast(metrics-> 'environment.inside.pressure' AS numeric) * 0.00029530 AS insideTemperature,\n cast(metrics-> 'environment.inside.fridge.pressure' AS numeric) * 0.00029530 AS fridgeTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n cast(metrics-> 'environment.outside.pressure' AS numeric) * 0.00029530 AS outsideTemperature,\n cast(metrics-> 'environment.inside.pressure' AS numeric) * 0.00029530 AS insideTemperature,\n cast(metrics-> 'environment.inside.fridge.pressure' AS numeric) * 0.00029530 AS fridgeTemperature\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -1742,7 +1742,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n anglespeedapparent\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n anglespeedapparent\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -1878,7 +1878,7 @@
|
||||
"group": [],
|
||||
"metricColumn": "none",
|
||||
"rawQuery": true,
|
||||
"rawSql": "SELECT\n time AS \"time\",\n windSpeedApparent\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND client_id = '${boat}'\nORDER BY 1",
|
||||
"rawSql": "SET vessel.id = '${__user.login}';\nSELECT\n time AS \"time\",\n windSpeedApparent\nFROM api.metrics\nWHERE\n $__timeFilter(time)\n AND vessel_id = '${boat}'\nORDER BY 1",
|
||||
"refId": "A",
|
||||
"select": [
|
||||
[
|
||||
@@ -1947,7 +1947,7 @@
|
||||
"type": "postgres",
|
||||
"uid": "PCC52D03280B7034C"
|
||||
},
|
||||
"definition": "SELECT\n v.name AS __text,\n m.client_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;",
|
||||
"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,
|
||||
@@ -1955,7 +1955,7 @@
|
||||
"multi": false,
|
||||
"name": "boat",
|
||||
"options": [],
|
||||
"query": "SELECT\n v.name AS __text,\n m.client_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;",
|
||||
"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,
|
||||
@@ -1981,7 +1981,7 @@
|
||||
},
|
||||
"timezone": "utc",
|
||||
"title": "Weather",
|
||||
"uid": "62bzzlr7z",
|
||||
"uid": "631a97c2e",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
134
grafana/dashboards/home.json
Normal file
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"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": ""
|
||||
}
|
@@ -11,3 +11,6 @@ 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
|
||||
|
@@ -7,7 +7,7 @@ providers:
|
||||
# <int> Org id. Default to 1
|
||||
orgId: 1
|
||||
# <string> name of the dashboard folder.
|
||||
folder: 'PostgSail'
|
||||
#folder: 'PostgSail'
|
||||
# <string> folder UID. will be automatically generated if not specified
|
||||
#folderUid: ''
|
||||
# <string> provider type. Default to 'file'
|
||||
@@ -15,7 +15,7 @@ providers:
|
||||
# <bool> disable dashboard deletion
|
||||
disableDeletion: false
|
||||
# <int> how often Grafana will scan for changed dashboards
|
||||
updateIntervalSeconds: 10
|
||||
updateIntervalSeconds: 60
|
||||
# <bool> allow updating provisioned dashboards from the UI
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
|
@@ -4,7 +4,7 @@ datasources:
|
||||
- name: PostgreSQL
|
||||
isDefault: true
|
||||
type: postgres
|
||||
url: 172.30.0.1:5432
|
||||
url: '${PGSAIL_GRAFANA_URI}'
|
||||
database: signalk
|
||||
user: grafana
|
||||
secureJsonData:
|
||||
@@ -14,5 +14,5 @@ datasources:
|
||||
maxOpenConns: 10 # Grafana v5.4+
|
||||
maxIdleConns: 2 # Grafana v5.4+
|
||||
connMaxLifetime: 14400 # Grafana v5.4+
|
||||
postgresVersion: 1400 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10
|
||||
postgresVersion: 1500 # 903=9.3, 904=9.4, 905=9.5, 906=9.6, 1000=10
|
||||
timescaledb: true
|
||||
|
76
initdb/01signalk.sql
Executable file
@@ -0,0 +1,76 @@
|
||||
---------------------------------------------------------------------------
|
||||
-- PostgSail => Postgres + TimescaleDB + PostGIS + PostgREST
|
||||
--
|
||||
-- Inspired from:
|
||||
-- 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
|
||||
-- CRON functions to process logbook, stays, moorages
|
||||
-- python functions for geo reverse and send notification via email and/or pushover
|
||||
-- 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
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
-- Drop database
|
||||
-- % docker exec -i timescaledb-postgis psql -Uusername -W postgres -c "drop database signalk;"
|
||||
|
||||
-- Import Schema
|
||||
-- % cat signalk.sql | docker exec -i timescaledb-postgis psql -Uusername postgres
|
||||
|
||||
-- Export hypertable
|
||||
-- % docker exec -i timescaledb-postgis psql -Uusername -W signalk -c "\COPY (SELECT * FROM api.metrics ORDER BY time ASC) TO '/var/lib/postgresql/data/metrics.csv' DELIMITER ',' CSV"
|
||||
-- Export hypertable to gzip
|
||||
-- # docker exec -i timescaledb-postgis psql -Uusername -W signalk -c "\COPY (SELECT * FROM api.metrics ORDER BY time ASC) TO PROGRAM 'gzip > /var/lib/postgresql/data/metrics.csv.gz' CSV HEADER;"
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
RAISE WARNING '
|
||||
_________.__ .__ ____ __.
|
||||
/ _____/|__| ____ ____ _____ | | | |/ _|
|
||||
\_____ \ | |/ ___\ / \\__ \ | | | <
|
||||
/ \| / /_/ > | \/ __ \| |_| | \
|
||||
/_______ /|__\___ /|___| (____ /____/____|__ \
|
||||
\/ /_____/ \/ \/ \/
|
||||
%', now();
|
||||
END $$;
|
||||
|
||||
select version();
|
||||
|
||||
-- Database
|
||||
CREATE DATABASE signalk;
|
||||
-- Limit connection to 100
|
||||
ALTER DATABASE signalk WITH CONNECTION LIMIT = 100;
|
||||
-- Set timezone to UTC
|
||||
ALTER DATABASE signalk SET TIMEZONE='UTC';
|
||||
|
||||
-- connect to the DB
|
||||
\c signalk
|
||||
|
||||
-- Schema
|
||||
CREATE SCHEMA IF NOT EXISTS api;
|
||||
COMMENT ON SCHEMA api IS 'api schema expose to postgrest';
|
||||
|
||||
-- Revoke default privileges to all public functions
|
||||
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
|
||||
|
||||
-- Extensions
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb; -- provides time series functions for PostgreSQL
|
||||
-- CREATE EXTENSION IF NOT EXISTS timescaledb_toolkit; -- provides time series functions for PostgreSQL
|
||||
CREATE EXTENSION IF NOT EXISTS postgis; -- adds support for geographic objects to the PostgreSQL object-relational database
|
||||
CREATE EXTENSION IF NOT EXISTS plpgsql; -- PL/pgSQL procedural language
|
||||
CREATE EXTENSION IF NOT EXISTS plpython3u; -- implements PL/Python based on the Python 3 language variant.
|
||||
CREATE EXTENSION IF NOT EXISTS jsonb_plpython3u CASCADE; -- tranform jsonb to python json type.
|
||||
CREATE EXTENSION IF NOT EXISTS pg_stat_statements; -- provides a means for tracking planning and execution statistics of all SQL statements executed
|
||||
CREATE EXTENSION IF NOT EXISTS "moddatetime"; -- provides functions for tracking last modification time
|
||||
|
||||
-- Trust plpython3u language by default
|
||||
UPDATE pg_language SET lanpltrusted = true WHERE lanname = 'plpython3u';
|
506
initdb/02_1_1_signalk_api_tables.sql
Normal file
@@ -0,0 +1,506 @@
|
||||
|
||||
-- connect to the DB
|
||||
\c signalk
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Tables
|
||||
--
|
||||
---------------------------------------------------------------------------
|
||||
-- Metadata from signalk
|
||||
CREATE TABLE IF NOT EXISTS api.metadata(
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NULL,
|
||||
mmsi NUMERIC NULL,
|
||||
client_id TEXT NULL,
|
||||
-- vessel_id link auth.vessels with api.metadata
|
||||
vessel_id TEXT NOT NULL UNIQUE,
|
||||
length DOUBLE PRECISION NULL,
|
||||
beam DOUBLE PRECISION NULL,
|
||||
height DOUBLE PRECISION NULL,
|
||||
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 !?
|
||||
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()
|
||||
);
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
api.metadata
|
||||
IS 'Stores metadata from vessel';
|
||||
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);
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Metrics from signalk
|
||||
-- Create vessel status enum
|
||||
CREATE TYPE status 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,
|
||||
client_id TEXT NULL,
|
||||
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
|
||||
latitude DOUBLE PRECISION NULL,
|
||||
longitude DOUBLE PRECISION NULL,
|
||||
speedOverGround DOUBLE PRECISION NULL,
|
||||
courseOverGroundTrue DOUBLE PRECISION NULL,
|
||||
windSpeedApparent DOUBLE PRECISION NULL,
|
||||
angleSpeedApparent DOUBLE PRECISION NULL,
|
||||
status status 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),
|
||||
PRIMARY KEY (time, vessel_id)
|
||||
);
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
api.metrics
|
||||
IS 'Stores metrics from vessel';
|
||||
COMMENT ON COLUMN api.metrics.latitude IS 'With CONSTRAINT but allow NULL value to be ignored silently by trigger';
|
||||
COMMENT ON COLUMN api.metrics.longitude IS 'With CONSTRAINT but allow NULL value to be ignored silently by trigger';
|
||||
|
||||
-- Index
|
||||
CREATE INDEX ON api.metrics (vessel_id, time DESC);
|
||||
CREATE INDEX ON api.metrics (status, time DESC);
|
||||
-- json index??
|
||||
CREATE INDEX ON api.metrics using GIN (metrics);
|
||||
-- timescaledb hypertable
|
||||
SELECT create_hypertable('api.metrics', 'time', chunk_time_interval => INTERVAL '7 day');
|
||||
-- timescaledb hypertable with space partitions
|
||||
-- ERROR: new row for relation "_hyper_1_2_chunk" violates check constraint "constraint_4"
|
||||
-- ((_timescaledb_internal.get_partition_hash(vessel_id) < 1073741823))
|
||||
--SELECT create_hypertable('api.metrics', 'time', 'vessel_id',
|
||||
-- number_partitions => 2,
|
||||
-- chunk_time_interval => INTERVAL '7 day',
|
||||
-- if_not_exists => true);
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Logbook
|
||||
-- todo add consumption fuel?
|
||||
-- todo add engine hour?
|
||||
-- todo add geom object http://epsg.io/4326 EPSG:4326 Unit: degres
|
||||
-- todo add geog object http://epsg.io/3857 EPSG:3857 Unit: meters
|
||||
-- https://postgis.net/workshops/postgis-intro/geography.html#using-geography
|
||||
-- https://medium.com/coord/postgis-performance-showdown-geometry-vs-geography-ec99967da4f0
|
||||
-- virtual logbook by boat by client_id impossible?
|
||||
-- https://www.postgresql.org/docs/current/ddl-partitioning.html
|
||||
-- Issue:
|
||||
-- https://www.reddit.com/r/PostgreSQL/comments/di5mbr/postgresql_12_foreign_keys_and_partitioned_tables/f3tsoop/
|
||||
-- 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,
|
||||
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
|
||||
active BOOLEAN DEFAULT false,
|
||||
name VARCHAR(255),
|
||||
_from VARCHAR(255),
|
||||
_from_lat DOUBLE PRECISION NULL,
|
||||
_from_lng DOUBLE PRECISION NULL,
|
||||
_to VARCHAR(255),
|
||||
_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,
|
||||
distance NUMERIC, -- meters?
|
||||
duration INTERVAL, -- duration in days and hours?
|
||||
avg_speed DOUBLE PRECISION NULL,
|
||||
max_speed DOUBLE PRECISION NULL,
|
||||
max_wind_speed DOUBLE PRECISION NULL,
|
||||
notes TEXT NULL,
|
||||
extra JSONB NULL
|
||||
);
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
api.logbook
|
||||
IS 'Stores generated logbook';
|
||||
COMMENT ON COLUMN api.logbook.distance IS 'in NM';
|
||||
|
||||
-- Index todo!
|
||||
CREATE INDEX logbook_vessel_id_idx ON api.logbook (vessel_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?';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- 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,
|
||||
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
|
||||
active BOOLEAN DEFAULT false,
|
||||
name VARCHAR(255),
|
||||
latitude DOUBLE PRECISION NULL,
|
||||
longitude DOUBLE PRECISION NULL,
|
||||
geog GEOGRAPHY(POINT) NULL,
|
||||
arrived TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
departed TIMESTAMP WITHOUT TIME ZONE,
|
||||
duration INTERVAL, -- duration in days and hours?
|
||||
stay_code INT DEFAULT 1, -- REFERENCES api.stays_at(stay_code),
|
||||
notes TEXT NULL
|
||||
);
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
api.stays
|
||||
IS 'Stores generated stays';
|
||||
|
||||
-- Index
|
||||
CREATE INDEX stays_vessel_id_idx ON api.stays (vessel_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.
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- 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,
|
||||
vessel_id TEXT NOT NULL REFERENCES api.metadata(vessel_id) ON DELETE RESTRICT,
|
||||
name TEXT,
|
||||
country TEXT, -- todo need to update reverse_geocode_py_fn
|
||||
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,
|
||||
latitude DOUBLE PRECISION NULL,
|
||||
longitude DOUBLE PRECISION NULL,
|
||||
geog GEOGRAPHY(POINT) NULL,
|
||||
home_flag BOOLEAN DEFAULT false,
|
||||
notes TEXT NULL
|
||||
);
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
api.moorages
|
||||
IS 'Stores generated moorages';
|
||||
|
||||
-- Index
|
||||
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.
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Stay Type
|
||||
CREATE TABLE IF NOT EXISTS api.stays_at(
|
||||
stay_code INTEGER NOT NULL,
|
||||
description TEXT NOT NULL
|
||||
);
|
||||
-- Description
|
||||
COMMENT ON TABLE api.stays_at IS 'Stay Type';
|
||||
-- Insert default possible values
|
||||
INSERT INTO api.stays_at(stay_code, description) VALUES
|
||||
(1, 'Unknow'),
|
||||
(2, 'Anchor'),
|
||||
(3, 'Mooring Buoy'),
|
||||
(4, 'Dock');
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Trigger Functions Metadata table
|
||||
--
|
||||
-- UPSERT - Insert vs Update for Metadata
|
||||
DROP FUNCTION IF EXISTS metadata_upsert_trigger_fn;
|
||||
CREATE FUNCTION metadata_upsert_trigger_fn() RETURNS trigger AS $metadata_upsert$
|
||||
DECLARE
|
||||
metadata_id integer;
|
||||
metadata_active boolean;
|
||||
BEGIN
|
||||
-- Set client_id to new value to allow RLS
|
||||
--PERFORM set_config('vessel.client_id', NEW.client_id, false);
|
||||
-- UPSERT - Insert vs Update for Metadata
|
||||
--RAISE NOTICE 'metadata_upsert_trigger_fn';
|
||||
--PERFORM set_config('vessel.id', NEW.vessel_id, true);
|
||||
--RAISE WARNING 'metadata_upsert_trigger_fn [%] [%]', current_setting('vessel.id', true), NEW;
|
||||
SELECT m.id,m.active INTO metadata_id, metadata_active
|
||||
FROM api.metadata m
|
||||
WHERE m.vessel_id IS NOT NULL AND m.vessel_id = current_setting('vessel.id', true);
|
||||
--RAISE NOTICE 'metadata_id is [%]', metadata_id;
|
||||
IF metadata_id IS NOT NULL THEN
|
||||
-- send notification if boat is back online
|
||||
IF metadata_active is False THEN
|
||||
-- Add monitor online entry to process queue for later notification
|
||||
INSERT INTO process_queue (channel, payload, stored, ref_id)
|
||||
VALUES ('monitoring_online', metadata_id, now(), current_setting('vessel.id', true));
|
||||
END IF;
|
||||
-- Update vessel metadata
|
||||
UPDATE api.metadata
|
||||
SET
|
||||
name = NEW.name,
|
||||
mmsi = NEW.mmsi,
|
||||
client_id = NEW.client_id,
|
||||
length = NEW.length,
|
||||
beam = NEW.beam,
|
||||
height = NEW.height,
|
||||
ship_type = NEW.ship_type,
|
||||
plugin_version = NEW.plugin_version,
|
||||
signalk_version = NEW.signalk_version,
|
||||
time = NEW.time,
|
||||
active = true
|
||||
WHERE id = metadata_id;
|
||||
RETURN NULL; -- Ignore insert
|
||||
ELSE
|
||||
IF NEW.vessel_id IS NULL THEN
|
||||
-- set vessel_id from jwt if not present in INSERT query
|
||||
NEW.vessel_id := current_setting('vessel.id');
|
||||
END IF;
|
||||
-- Insert new vessel metadata and
|
||||
RETURN NEW; -- Insert new vessel metadata
|
||||
END IF;
|
||||
END;
|
||||
$metadata_upsert$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.metadata_upsert_trigger_fn
|
||||
IS 'process metadata from vessel, upsert';
|
||||
|
||||
CREATE TRIGGER metadata_moddatetime
|
||||
BEFORE UPDATE ON api.metadata
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE moddatetime (updated_at);
|
||||
-- Description
|
||||
COMMENT ON TRIGGER metadata_moddatetime
|
||||
ON api.metadata
|
||||
IS 'Automatic update of updated_at on table modification';
|
||||
|
||||
-- FUNCTION Metadata notification for new vessel after insert
|
||||
DROP FUNCTION IF EXISTS metadata_notification_trigger_fn;
|
||||
CREATE FUNCTION metadata_notification_trigger_fn() RETURNS trigger AS $metadata_notification$
|
||||
DECLARE
|
||||
BEGIN
|
||||
RAISE NOTICE 'metadata_notification_trigger_fn [%]', NEW;
|
||||
INSERT INTO process_queue (channel, payload, stored, ref_id)
|
||||
VALUES ('monitoring_online', NEW.id, now(), NEW.vessel_id);
|
||||
RETURN NULL;
|
||||
END;
|
||||
$metadata_notification$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.metadata_notification_trigger_fn
|
||||
IS 'process metadata notification from vessel, monitoring_online';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Trigger metadata table
|
||||
--
|
||||
-- Metadata trigger BEFORE INSERT
|
||||
CREATE TRIGGER metadata_upsert_trigger BEFORE INSERT ON api.metadata
|
||||
FOR EACH ROW EXECUTE FUNCTION metadata_upsert_trigger_fn();
|
||||
-- Description
|
||||
COMMENT ON TRIGGER
|
||||
metadata_upsert_trigger ON api.metadata
|
||||
IS 'BEFORE INSERT ON api.metadata run function metadata_upsert_trigger_fn';
|
||||
|
||||
-- Metadata trigger AFTER INSERT
|
||||
CREATE TRIGGER metadata_notification_trigger AFTER INSERT ON api.metadata
|
||||
FOR EACH ROW EXECUTE FUNCTION metadata_notification_trigger_fn();
|
||||
-- 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';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Trigger Functions metrics table
|
||||
--
|
||||
-- Create a logbook or stay entry base on the vessel state, eg: navigation.state
|
||||
-- https://github.com/meri-imperiumi/signalk-autostate
|
||||
|
||||
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;
|
||||
_vessel_id TEXT;
|
||||
BEGIN
|
||||
--RAISE NOTICE 'metrics_trigger_fn';
|
||||
--RAISE WARNING 'metrics_trigger_fn [%] [%]', current_setting('vessel.id', true), NEW;
|
||||
-- Ensure vessel.id to new value to allow RLS
|
||||
IF NEW.vessel_id IS NULL THEN
|
||||
-- set vessel_id from jwt if not present in INSERT query
|
||||
NEW.vessel_id := current_setting('vessel.id');
|
||||
END IF;
|
||||
-- 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
|
||||
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
|
||||
-- Ignore entry if same time
|
||||
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], duplicate time [%] = [%]', NEW.vessel_id, previous_time, NEW.time;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
IF previous_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 [%] > [%]', NEW.vessel_id, previous_time, NEW.time;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
-- Check if latitude or longitude are null
|
||||
IF NEW.latitude IS NULL OR NEW.longitude IS NULL THEN
|
||||
-- Ignore entry if null latitude,longitude
|
||||
RAISE WARNING 'Metrics Ignoring metric, vessel_id [%], null latitude,longitude [%] [%]', NEW.vessel_id, NEW.latitude, NEW.longitude;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
-- Check if status is null
|
||||
IF 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 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;
|
||||
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';
|
||||
END IF;
|
||||
-- Add new stay as no previous entry exist
|
||||
INSERT INTO api.stays
|
||||
(vessel_id, active, arrived, latitude, longitude, stay_code)
|
||||
VALUES (current_setting('vessel.id', true), true, NEW.time, NEW.latitude, NEW.longitude, 1)
|
||||
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 first stay as no previous metrics exist, stay_id %', stay_id;
|
||||
END IF;
|
||||
-- Check if status is valid enum
|
||||
SELECT NEW.status::name = any(enum_range(null::status)::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;
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
-- 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;
|
||||
-- Start new log
|
||||
logbook_id := public.trip_in_progress_fn(current_setting('vessel.id', true)::TEXT);
|
||||
IF logbook_id IS NULL THEN
|
||||
INSERT INTO api.logbook
|
||||
(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;
|
||||
ELSE
|
||||
UPDATE api.logbook
|
||||
SET
|
||||
active = false,
|
||||
_to_time = NEW.time,
|
||||
_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;
|
||||
END IF;
|
||||
|
||||
-- End current stay
|
||||
stay_id := public.stay_in_progress_fn(current_setting('vessel.id', true)::TEXT);
|
||||
IF stay_id IS NOT NULL THEN
|
||||
UPDATE api.stays
|
||||
SET
|
||||
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
|
||||
INSERT INTO process_queue (channel, payload, stored, ref_id)
|
||||
VALUES ('new_moorage', stay_id, now(), current_setting('vessel.id', true));
|
||||
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
|
||||
-- Start new stays
|
||||
RAISE WARNING 'Metrics Update status, try new stay, New:[%] Previous:[%]', NEW.status, previous_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;
|
||||
-- If metric status is anchored set stay_code accordingly
|
||||
stay_code = 1;
|
||||
IF NEW.status = 'anchored' THEN
|
||||
stay_code = 2;
|
||||
END IF;
|
||||
-- Add new stay
|
||||
INSERT INTO api.stays
|
||||
(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));
|
||||
ELSE
|
||||
RAISE WARNING 'Metrics Invalid stay_id [%] [%]', stay_id, NEW.time;
|
||||
UPDATE api.stays
|
||||
SET
|
||||
active = false,
|
||||
departed = NEW.time
|
||||
WHERE id = stay_id;
|
||||
END IF;
|
||||
|
||||
-- End current log/trip
|
||||
-- Fetch logbook_id by vessel_id
|
||||
logbook_id := public.trip_in_progress_fn(current_setting('vessel.id', true)::TEXT);
|
||||
IF logbook_id IS NOT NULL THEN
|
||||
-- todo check on time start vs end
|
||||
RAISE WARNING 'Metrics Updating logbook status [%] [%] [%]', logbook_id, NEW.status, NEW.time;
|
||||
UPDATE api.logbook
|
||||
SET
|
||||
active = false,
|
||||
_to_time = NEW.time,
|
||||
_to_lat = NEW.latitude,
|
||||
_to_lng = NEW.longitude
|
||||
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));
|
||||
ELSE
|
||||
RAISE WARNING 'Metrics Invalid logbook_id [%] [%]', logbook_id, NEW.time;
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW; -- Finally insert the actual new metric
|
||||
END;
|
||||
$metrics$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.metrics_trigger_fn
|
||||
IS 'process metrics from vessel, generate new_logbook and new_stay.';
|
||||
|
||||
--
|
||||
-- Triggers logbook update on metrics insert
|
||||
CREATE TRIGGER metrics_trigger BEFORE INSERT ON api.metrics
|
||||
FOR EACH ROW EXECUTE FUNCTION metrics_trigger_fn();
|
||||
-- Description
|
||||
COMMENT ON TRIGGER
|
||||
metrics_trigger ON api.metrics
|
||||
IS 'BEFORE INSERT ON api.metrics run function metrics_trigger_fn';
|
381
initdb/02_1_2_signalk_api_functions.sql
Normal file
@@ -0,0 +1,381 @@
|
||||
|
||||
-- connect to the DB
|
||||
\c signalk
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- API helper functions
|
||||
--
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Functions API schema
|
||||
-- Timelapse - replay logs
|
||||
DROP FUNCTION IF EXISTS api.timelapse_fn;
|
||||
CREATE OR REPLACE FUNCTION api.timelapse_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 JSON) RETURNS JSON AS $timelapse$
|
||||
DECLARE
|
||||
_geojson jsonb;
|
||||
BEGIN
|
||||
-- TODO using jsonb pgsql function instead of python
|
||||
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;
|
||||
--raise WARNING 'by log _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';
|
||||
--raise WARNING 'by date _geojson %' , _geojson;
|
||||
ELSE
|
||||
SELECT jsonb_agg(track_geojson->'features') INTO _geojson
|
||||
FROM api.logbook;
|
||||
--raise WARNING 'all result _geojson %' , _geojson;
|
||||
END IF;
|
||||
-- Return a GeoJSON filter on Point
|
||||
-- result _geojson [null, null]
|
||||
--raise WARNING 'result _geojson %' , _geojson;
|
||||
SELECT json_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', public.geojson_py_fn(_geojson, 'LineString'::TEXT) ) INTO geojson;
|
||||
END;
|
||||
$timelapse$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.timelapse_fn
|
||||
IS 'Export to geojson feature point with Time and courseOverGroundTrue 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$
|
||||
-- validate with geojson.io
|
||||
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_geojson_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_geojson_fn invalid logbook %', _id;
|
||||
RETURN;
|
||||
END IF;
|
||||
geojson := logbook_rec.track_geojson;
|
||||
END;
|
||||
$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';
|
||||
|
||||
-- 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) RETURNS pg_catalog.xml
|
||||
AS $export_logbook_gpx$
|
||||
DECLARE
|
||||
log_rec record;
|
||||
BEGIN
|
||||
-- If _id is is not NULL and > 0
|
||||
IF _id IS NULL OR _id < 1 THEN
|
||||
RAISE WARNING '-> export_logbook_geojson_fn invalid input %', _id;
|
||||
RETURN '';
|
||||
END IF;
|
||||
-- Gather log details _from_time and _to_time
|
||||
SELECT * INTO log_rec
|
||||
FROM
|
||||
api.logbook l
|
||||
WHERE l.id = _id;
|
||||
-- Ensure the query is successful
|
||||
IF log_rec.vessel_id IS NULL THEN
|
||||
RAISE WARNING '-> export_logbook_gpx_fn invalid logbook %', _id;
|
||||
RETURN '';
|
||||
END IF;
|
||||
-- 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",
|
||||
'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 trk,
|
||||
xmlelement(name name, log_rec.name),
|
||||
xmlelement(name desc, log_rec.notes),
|
||||
xmlelement(name link, xmlattributes(concat('https://iot.openplotter.cloud/log/', log_rec.id) as href),
|
||||
xmlelement(name text, log_rec.name)),
|
||||
xmlelement(name extensions, xmlelement(name "postgsail:log_id", 1),
|
||||
xmlelement(name "postgsail:link", concat('https://iot.openplotter.cloud/log/', log_rec.id)),
|
||||
xmlelement(name "opencpn:guid", uuid_generate_v4()),
|
||||
xmlelement(name "opencpn:viz", '1'),
|
||||
xmlelement(name "opencpn:start", log_rec._from_time),
|
||||
xmlelement(name "opencpn:end", log_rec._to_time)
|
||||
),
|
||||
xmlelement(name trkseg, xmlagg(
|
||||
xmlelement(name trkpt,
|
||||
xmlattributes(latitude as lat, longitude as lon),
|
||||
xmlelement(name time, time)
|
||||
)))))::pg_catalog.xml
|
||||
FROM api.metrics m
|
||||
WHERE m.latitude IS NOT NULL
|
||||
AND m.longitude IS NOT NULL
|
||||
AND m.time >= log_rec._from_time::TIMESTAMP WITHOUT TIME ZONE
|
||||
AND m.time <= log_rec._to_time::TIMESTAMP WITHOUT TIME ZONE
|
||||
AND vessel_id = log_rec.vessel_id;
|
||||
-- ERROR: column "m.time" must appear in the GROUP BY clause or be used in an aggregate function at character 2304
|
||||
--ORDER BY m.time ASC;
|
||||
END;
|
||||
$export_logbook_gpx$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.export_logbook_gpx_fn
|
||||
IS 'Export a log entry to GPX 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$
|
||||
DECLARE
|
||||
moorage_rec record;
|
||||
_geojson jsonb;
|
||||
BEGIN
|
||||
-- If _id is is not NULL and > 0
|
||||
IF _id IS NULL OR _id < 1 THEN
|
||||
RAISE WARNING '-> find_log_from_moorage_fn invalid input %', _id;
|
||||
RETURN;
|
||||
END IF;
|
||||
-- Gather moorage details
|
||||
SELECT * INTO moorage_rec
|
||||
FROM api.moorages m
|
||||
WHERE m.id = _id;
|
||||
-- Find all log from and to moorage geopoint within 100m
|
||||
SELECT jsonb_agg(l.track_geojson->'features') INTO _geojson
|
||||
FROM api.logbook l
|
||||
WHERE ST_DWithin(
|
||||
Geography(ST_MakePoint(l._from_lng, l._from_lat)),
|
||||
moorage_rec.geog,
|
||||
1000 -- in meters ?
|
||||
);
|
||||
-- Return a GeoJSON filter on LineString
|
||||
SELECT json_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', public.geojson_py_fn(_geojson, 'Point'::TEXT) ) INTO geojson;
|
||||
END;
|
||||
$find_log_from_moorage$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.find_log_from_moorage_fn
|
||||
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$
|
||||
DECLARE
|
||||
moorage_rec record;
|
||||
_geojson jsonb;
|
||||
BEGIN
|
||||
-- If _id is is not NULL and > 0
|
||||
IF _id IS NULL OR _id < 1 THEN
|
||||
RAISE WARNING '-> find_log_from_moorage_fn invalid input %', _id;
|
||||
RETURN;
|
||||
END IF;
|
||||
-- Gather moorage details
|
||||
SELECT * INTO moorage_rec
|
||||
FROM api.moorages m
|
||||
WHERE m.id = _id;
|
||||
-- Find all log from and to moorage geopoint within 100m
|
||||
SELECT jsonb_agg(l.track_geojson->'features') INTO _geojson
|
||||
FROM api.logbook l
|
||||
WHERE ST_DWithin(
|
||||
Geography(ST_MakePoint(l._to_lng, l._to_lat)),
|
||||
moorage_rec.geog,
|
||||
1000 -- in meters ?
|
||||
);
|
||||
-- Return a GeoJSON filter on LineString
|
||||
SELECT json_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', public.geojson_py_fn(_geojson, 'Point'::TEXT) ) INTO geojson;
|
||||
END;
|
||||
$find_log_to_moorage$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.find_log_to_moorage_fn
|
||||
IS 'Find all log to moorage geopoint within 100m';
|
||||
|
||||
-- Find all stay within 100m of moorage geopoint
|
||||
DROP FUNCTION IF EXISTS api.find_stay_from_moorage_fn;
|
||||
CREATE OR REPLACE FUNCTION api.find_stay_from_moorage_fn(IN _id INTEGER) RETURNS void AS $find_stay_from_moorage$
|
||||
DECLARE
|
||||
moorage_rec record;
|
||||
stay_rec record;
|
||||
BEGIN
|
||||
-- If _id is is not NULL and > 0
|
||||
SELECT * INTO moorage_rec
|
||||
FROM api.moorages m
|
||||
WHERE m.id = _id;
|
||||
-- find all log from and to moorage geopoint within 100m
|
||||
--RETURN QUERY
|
||||
SELECT s.id,s.arrived,s.departed,s.duration,sa.description
|
||||
FROM api.stays s, api.stays_at sa
|
||||
WHERE ST_DWithin(
|
||||
s.geog,
|
||||
moorage_rec.geog,
|
||||
100 -- in meters ?
|
||||
)
|
||||
AND departed IS NOT NULL
|
||||
AND s.name IS NOT NULL
|
||||
AND s.stay_code = sa.stay_code
|
||||
ORDER BY s.arrived DESC;
|
||||
END;
|
||||
$find_stay_from_moorage$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.find_stay_from_moorage_fn
|
||||
IS 'Find all stay within 100m of moorage geopoint';
|
||||
|
||||
-- trip_in_progress_fn
|
||||
DROP FUNCTION IF EXISTS public.trip_in_progress_fn;
|
||||
CREATE FUNCTION public.trip_in_progress_fn(IN _vessel_id TEXT) RETURNS INT AS $trip_in_progress$
|
||||
DECLARE
|
||||
logbook_id INT := NULL;
|
||||
BEGIN
|
||||
SELECT id INTO logbook_id
|
||||
FROM api.logbook l
|
||||
WHERE l.vessel_id IS NOT NULL
|
||||
AND l.vessel_id = _vessel_id
|
||||
AND active IS true
|
||||
LIMIT 1;
|
||||
RETURN logbook_id;
|
||||
END;
|
||||
$trip_in_progress$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.trip_in_progress_fn
|
||||
IS 'trip_in_progress';
|
||||
|
||||
-- stay_in_progress_fn
|
||||
DROP FUNCTION IF EXISTS public.stay_in_progress_fn;
|
||||
CREATE FUNCTION public.stay_in_progress_fn(IN _vessel_id TEXT) RETURNS INT AS $stay_in_progress$
|
||||
DECLARE
|
||||
stay_id INT := NULL;
|
||||
BEGIN
|
||||
SELECT id INTO stay_id
|
||||
FROM api.stays s
|
||||
WHERE s.vessel_id IS NOT NULL
|
||||
AND s.vessel_id = _vessel_id
|
||||
AND active IS true
|
||||
LIMIT 1;
|
||||
RETURN stay_id;
|
||||
END;
|
||||
$stay_in_progress$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.stay_in_progress_fn
|
||||
IS 'stay_in_progress';
|
||||
|
||||
-- logs_by_month_fn
|
||||
DROP FUNCTION IF EXISTS api.logs_by_month_fn;
|
||||
CREATE FUNCTION api.logs_by_month_fn(OUT charts JSONB) RETURNS JSONB AS $logs_by_month$
|
||||
DECLARE
|
||||
data JSONB;
|
||||
BEGIN
|
||||
-- Query logs by month
|
||||
SELECT json_object_agg(month,count) INTO data
|
||||
FROM (
|
||||
SELECT
|
||||
to_char(date_trunc('month', _from_time), 'MM') as month,
|
||||
count(*) as count
|
||||
FROM api.logbook
|
||||
GROUP BY month
|
||||
ORDER BY month
|
||||
) AS t;
|
||||
-- Merge jsonb to get all 12 months
|
||||
SELECT '{"01": 0, "02": 0, "03": 0, "04": 0, "05": 0, "06": 0, "07": 0, "08": 0, "09": 0, "10": 0, "11": 0,"12": 0}'::jsonb ||
|
||||
data::jsonb INTO charts;
|
||||
END;
|
||||
$logs_by_month$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.logs_by_month_fn
|
||||
IS 'logbook by month for web charts';
|
||||
|
||||
-- moorage_geojson_fn
|
||||
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(
|
||||
'type', 'FeatureCollection',
|
||||
'features',
|
||||
( SELECT
|
||||
json_agg(ST_AsGeoJSON(m.*)::JSON) as moorages_geojson
|
||||
FROM
|
||||
( SELECT
|
||||
id,name,
|
||||
EXTRACT(DAY FROM justify_hours ( stay_duration )) AS Total_Stay,
|
||||
geog
|
||||
FROM api.moorages
|
||||
) AS m
|
||||
)
|
||||
) INTO geojson;
|
||||
END;
|
||||
$export_moorages_geojson$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.export_moorages_geojson_fn
|
||||
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$
|
||||
DECLARE
|
||||
BEGIN
|
||||
-- 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",
|
||||
'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"),
|
||||
xmlagg(
|
||||
xmlelement(name wpt, xmlattributes(m.latitude as lat, m.longitude as lon),
|
||||
xmlelement(name name, m.name),
|
||||
xmlelement(name time, 'TODO first seen'),
|
||||
xmlelement(name desc,
|
||||
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)),
|
||||
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 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 "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)
|
||||
))))
|
||||
)::pg_catalog.xml
|
||||
FROM api.moorages m;
|
||||
END;
|
||||
$export_moorages_gpx$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.export_moorages_gpx_fn
|
||||
IS 'Export moorages as gpx';
|
452
initdb/02_1_3_signalk_api_views.sql
Normal file
@@ -0,0 +1,452 @@
|
||||
|
||||
-- connect to the DB
|
||||
\c signalk
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- API helper views
|
||||
--
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- Views
|
||||
-- Views are invoked with the privileges of the view owner,
|
||||
-- make the user_role the view’s owner.
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
CREATE VIEW first_metric AS
|
||||
SELECT *
|
||||
FROM api.metrics
|
||||
ORDER BY time ASC LIMIT 1;
|
||||
|
||||
CREATE VIEW last_metric AS
|
||||
SELECT *
|
||||
FROM api.metrics
|
||||
ORDER BY time DESC LIMIT 1;
|
||||
|
||||
CREATE VIEW trip_in_progress AS
|
||||
SELECT *
|
||||
FROM api.logbook
|
||||
WHERE active IS true;
|
||||
|
||||
CREATE VIEW stay_in_progress AS
|
||||
SELECT *
|
||||
FROM api.stays
|
||||
WHERE active IS true;
|
||||
|
||||
-- TODO: Use materialized views instead as it is not live data
|
||||
-- Logs web view
|
||||
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"
|
||||
FROM api.logbook l
|
||||
WHERE _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
|
||||
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"
|
||||
FROM api.logbook l
|
||||
WHERE _to_time IS NOT NULL
|
||||
ORDER BY _from_time DESC;
|
||||
-- Description
|
||||
COMMENT ON MATERIALIZED VIEW
|
||||
api.logs_mat_view
|
||||
IS 'Logs MATERIALIZED web 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",
|
||||
track_geojson as geojson,
|
||||
avg_speed as avg_speed,
|
||||
max_speed as max_speed,
|
||||
max_wind_speed as max_wind_speed,
|
||||
extra as extra
|
||||
FROM api.logbook l
|
||||
WHERE _to_time IS NOT NULL
|
||||
ORDER BY _from_time DESC;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
api.log_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",
|
||||
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",
|
||||
s.departed AS "departed",
|
||||
s.notes AS "notes"
|
||||
FROM api.stays s, api.stays_at sa, api.moorages m
|
||||
WHERE departed IS NOT NULL
|
||||
AND s.name IS NOT NULL
|
||||
AND s.stay_code = sa.stay_code
|
||||
AND s.id = m.stay_id
|
||||
ORDER BY s.arrived DESC;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
api.stays_view
|
||||
IS 'Stays web 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",
|
||||
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",
|
||||
s.departed AS "departed",
|
||||
s.notes AS "notes"
|
||||
FROM api.stays s, api.stays_at sa, api.moorages m
|
||||
WHERE departed IS NOT NULL
|
||||
AND s.name IS NOT NULL
|
||||
AND s.stay_code = sa.stay_code
|
||||
AND s.id = m.stay_id
|
||||
ORDER BY s.arrived DESC;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
api.stay_view
|
||||
IS 'Stay web view';
|
||||
|
||||
-- Moorages web view
|
||||
-- TODO, this is wrong using distinct (m.name) should be using postgis geog feature
|
||||
--DROP VIEW IF EXISTS api.moorages_view_old;
|
||||
--CREATE VIEW api.moorages_view_old AS
|
||||
-- SELECT
|
||||
-- m.name AS Moorage,
|
||||
-- sa.description AS "Default Stay",
|
||||
-- sum((m.departed-m.arrived)) OVER (PARTITION by m.name) AS "Total Stay",
|
||||
-- count(m.departed) OVER (PARTITION by m.name) AS "Arrivals & Departures"
|
||||
-- FROM api.moorages m, api.stays_at sa
|
||||
-- WHERE departed is not null
|
||||
-- AND m.name is not null
|
||||
-- AND m.stay_code = sa.stay_code
|
||||
-- GROUP BY m.name,sa.description,m.departed,m.arrived
|
||||
-- ORDER BY 4 DESC;
|
||||
|
||||
-- the good way?
|
||||
DROP VIEW IF EXISTS api.moorages_view;
|
||||
CREATE OR REPLACE VIEW api.moorages_view WITH (security_invoker=true,security_barrier=true) AS -- TODO
|
||||
SELECT m.id,
|
||||
m.name AS Moorage,
|
||||
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.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 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;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
api.moorages_view
|
||||
IS 'Moorages listing web view';
|
||||
|
||||
DROP VIEW IF EXISTS api.moorage_view;
|
||||
CREATE OR REPLACE VIEW api.moorage_view WITH (security_invoker=true,security_barrier=true) AS -- TODO
|
||||
SELECT id,
|
||||
m.name AS Name,
|
||||
m.stay_code AS Default_Stay,
|
||||
m.home_flag AS Home,
|
||||
EXTRACT(DAY FROM justify_hours ( m.stay_duration )) AS Total_Stay,
|
||||
m.reference_count AS Arrivals_Departures,
|
||||
m.notes
|
||||
-- m.geog
|
||||
FROM api.moorages m
|
||||
WHERE m.name IS NOT NULL;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
api.moorage_view
|
||||
IS 'Moorage details 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
|
||||
-- m.name, ST_MakePoint(m._lng,m._lat),
|
||||
-- l._from, ST_MakePoint(l._from_lng,l._from_lat),
|
||||
-- ST_DistanceSphere(ST_MakePoint(m._lng,m._lat), ST_MakePoint(l._from_lng,l._from_lat))
|
||||
-- FROM api.moorages m , api.logbook l
|
||||
-- WHERE ST_DistanceSphere(ST_MakePoint(m._lng,m._lat), ST_MakePoint(l._from_lng,l._from_lat)) <= 100;
|
||||
|
||||
-- Stats web view
|
||||
-- TODO....
|
||||
-- first time entry from metrics
|
||||
----> select * from api.metrics m ORDER BY m.time desc limit 1
|
||||
-- last time entry from metrics
|
||||
----> select * from api.metrics m ORDER BY m.time asc limit 1
|
||||
-- max speed from logbook
|
||||
-- max wind speed from logbook
|
||||
----> select max(l.max_speed) as max_speed, max(l.max_wind_speed) as max_wind_speed from api.logbook l;
|
||||
-- Total Distance from logbook
|
||||
----> select sum(l.distance) as "Total Distance" from api.logbook l;
|
||||
-- Total Time Underway from logbook
|
||||
----> select sum(l.duration) as "Total Time Underway" from api.logbook l;
|
||||
-- Longest Nonstop Sail from logbook, eg longest trip duration and distance
|
||||
----> select max(l.duration),max(l.distance) from api.logbook l;
|
||||
CREATE OR REPLACE VIEW api.stats_logs_view WITH (security_invoker=true,security_barrier=true) AS -- TODO
|
||||
WITH
|
||||
meta AS (
|
||||
SELECT m.name FROM api.metadata m ),
|
||||
last_metric AS (
|
||||
SELECT m.time FROM api.metrics m ORDER BY m.time DESC limit 1),
|
||||
first_metric AS (
|
||||
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"
|
||||
FROM api.logbook l)
|
||||
SELECT
|
||||
m.name as Name,
|
||||
fm.time AS first,
|
||||
lm.time AS last,
|
||||
l.*
|
||||
FROM first_metric fm, last_metric lm, logbook l, meta m;
|
||||
COMMENT ON VIEW
|
||||
api.stats_logs_view
|
||||
IS 'Statistics Logs web view';
|
||||
|
||||
-- Home Ports / Unique Moorages
|
||||
----> select count(*) as "Home Ports" from api.moorages m where home_flag is true;
|
||||
-- Unique Moorages
|
||||
----> select count(*) as "Home Ports" from api.moorages m;
|
||||
-- Time Spent at Home Port(s)
|
||||
----> select sum(m.stay_duration) as "Time Spent at Home Port(s)" from api.moorages m where home_flag is true;
|
||||
-- OR
|
||||
----> select m.stay_duration as "Time Spent at Home Port(s)" from api.moorages m where home_flag is true;
|
||||
-- Time Spent Away
|
||||
----> select sum(m.stay_duration) as "Time Spent Away" from api.moorages m where home_flag is false;
|
||||
-- Time Spent Away order by, group by stay_code (Dock, Anchor, Mooring Buoys, Unclassified)
|
||||
----> select sa.description,sum(m.stay_duration) as "Time Spent Away" from api.moorages m, api.stays_at sa where home_flag is false AND m.stay_code = sa.stay_code group by m.stay_code,sa.description order by m.stay_code;
|
||||
CREATE OR REPLACE VIEW api.stats_moorages_view WITH (security_invoker=true,security_barrier=true) AS -- TODO
|
||||
WITH
|
||||
home_ports AS (
|
||||
select count(*) as home_ports from api.moorages m where home_flag is true
|
||||
),
|
||||
unique_moorage AS (
|
||||
select count(*) as unique_moorage from api.moorages m
|
||||
),
|
||||
time_at_home_ports AS (
|
||||
select sum(m.stay_duration) as time_at_home_ports from api.moorages m where home_flag is true
|
||||
),
|
||||
time_spent_away AS (
|
||||
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"
|
||||
FROM home_ports, unique_moorage, time_at_home_ports, time_spent_away;
|
||||
COMMENT ON VIEW
|
||||
api.stats_moorages_view
|
||||
IS 'Statistics Moorages web view';
|
||||
|
||||
CREATE OR REPLACE VIEW api.stats_moorages_away_view WITH (security_invoker=true,security_barrier=true) AS -- TODO
|
||||
SELECT sa.description,sum(m.stay_duration) as time_spent_away_by
|
||||
FROM api.moorages m, api.stays_at sa
|
||||
WHERE home_flag IS false
|
||||
AND m.stay_code = sa.stay_code
|
||||
GROUP BY m.stay_code,sa.description
|
||||
ORDER BY m.stay_code;
|
||||
COMMENT ON VIEW
|
||||
api.stats_moorages_away_view
|
||||
IS 'Statistics Moorages Time Spent Away web view';
|
||||
|
||||
--CREATE VIEW api.stats_view AS -- todo
|
||||
-- WITH
|
||||
-- logs AS (
|
||||
-- SELECT * FROM api.stats_logs_view ),
|
||||
-- moorages AS (
|
||||
-- SELECT * FROM api.stats_moorages_view)
|
||||
-- SELECT
|
||||
-- l.*,
|
||||
-- m.*
|
||||
-- FROM logs l, moorages m;
|
||||
--COMMENT ON VIEW
|
||||
-- api.stats_moorages_away_view
|
||||
-- IS 'Statistics Moorages Time Spent Away web view';
|
||||
|
||||
-- View main monitoring for web app
|
||||
DROP VIEW IF EXISTS api.monitoring_view;
|
||||
CREATE VIEW api.monitoring_view WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT
|
||||
time AS "time",
|
||||
(NOW() AT TIME ZONE 'UTC' - time) > INTERVAL '70 MINUTES' as offline,
|
||||
metrics-> 'environment.water.temperature' AS waterTemperature,
|
||||
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.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,
|
||||
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
|
||||
)::jsonb ) AS geojson,
|
||||
current_setting('vessel.name', false) AS name
|
||||
FROM api.metrics m
|
||||
ORDER BY time DESC LIMIT 1;
|
||||
COMMENT ON VIEW
|
||||
api.monitoring_view
|
||||
IS 'Monitoring static web view';
|
||||
|
||||
DROP VIEW IF EXISTS api.monitoring_humidity;
|
||||
CREATE VIEW api.monitoring_humidity WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT m.time, key, value
|
||||
FROM api.metrics m,
|
||||
jsonb_each_text(m.metrics)
|
||||
WHERE key ILIKE 'environment.%.humidity'
|
||||
ORDER BY m.time DESC;
|
||||
COMMENT ON VIEW
|
||||
api.monitoring_humidity
|
||||
IS 'Monitoring environment.%.humidity web view';
|
||||
|
||||
-- View System RPI monitoring for grafana
|
||||
-- View Electric monitoring for grafana
|
||||
|
||||
-- View main monitoring for grafana
|
||||
-- LAST Monitoring data from json!
|
||||
DROP VIEW IF EXISTS api.monitoring_temperatures;
|
||||
CREATE VIEW api.monitoring_temperatures WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT m.time, key, value
|
||||
FROM api.metrics m,
|
||||
jsonb_each_text(m.metrics)
|
||||
WHERE key ILIKE 'environment.%.temperature'
|
||||
ORDER BY m.time DESC;
|
||||
COMMENT ON VIEW
|
||||
api.monitoring_temperatures
|
||||
IS 'Monitoring environment.%.temperature web view';
|
||||
|
||||
-- json key regexp
|
||||
-- https://stackoverflow.com/questions/38204467/selecting-for-a-jsonb-array-contains-regex-match
|
||||
-- Last voltage data from json!
|
||||
DROP VIEW IF EXISTS api.monitoring_voltage;
|
||||
CREATE VIEW api.monitoring_voltage WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT m.time, key, value
|
||||
FROM api.metrics m,
|
||||
jsonb_each_text(m.metrics)
|
||||
WHERE key ILIKE 'electrical.%.voltage'
|
||||
ORDER BY m.time DESC;
|
||||
COMMENT ON VIEW
|
||||
api.monitoring_voltage
|
||||
IS 'Monitoring electrical.%.voltage web view';
|
||||
|
||||
-- Last whatever data from json!
|
||||
DROP VIEW IF EXISTS api.monitoring_view2;
|
||||
CREATE VIEW api.monitoring_view2 WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
jsonb_each(
|
||||
( SELECT metrics FROM api.metrics m ORDER BY time DESC LIMIT 1)
|
||||
);
|
||||
-- WHERE key ilike 'tanks.%.capacity%'
|
||||
-- or key ilike 'electrical.solar.%.panelPower'
|
||||
-- or key ilike 'electrical.batteries%stateOfCharge'
|
||||
-- or key ilike 'tanks\.%currentLevel'
|
||||
COMMENT ON VIEW
|
||||
api.monitoring_view2
|
||||
IS 'Monitoring Last whatever data from json web view';
|
||||
|
||||
-- Timeseries whatever data from json!
|
||||
DROP VIEW IF EXISTS api.monitoring_view3;
|
||||
CREATE VIEW api.monitoring_view3 WITH (security_invoker=true,security_barrier=true) AS
|
||||
SELECT m.time, key, value
|
||||
FROM api.metrics m,
|
||||
jsonb_each_text(m.metrics)
|
||||
ORDER BY m.time DESC;
|
||||
-- WHERE key ILIKE 'electrical.batteries%voltage';
|
||||
-- WHERE key ilike 'tanks.%.capacity%'
|
||||
-- or key ilike 'electrical.solar.%.panelPower'
|
||||
-- or key ilike 'electrical.batteries%stateOfCharge';
|
||||
-- key ILIKE 'propulsion.%.runTime'
|
||||
-- key ILIKE 'navigation.log'
|
||||
COMMENT ON VIEW
|
||||
api.monitoring_view3
|
||||
IS 'Monitoring Timeseries whatever data from json web view';
|
||||
|
||||
-- Infotiles web app
|
||||
DROP VIEW IF EXISTS api.total_info_view;
|
||||
CREATE VIEW api.total_info_view WITH (security_invoker=true,security_barrier=true) AS
|
||||
-- Infotiles web app, not used calculated client side
|
||||
WITH
|
||||
l as (SELECT count(*) as logs FROM api.logbook),
|
||||
s as (SELECT count(*) as stays FROM api.stays),
|
||||
m as (SELECT count(*) as moorages FROM api.moorages)
|
||||
SELECT * FROM l,s,m;
|
||||
COMMENT ON VIEW
|
||||
api.total_info_view
|
||||
IS 'total_info_view web view';
|
@@ -18,7 +18,7 @@ begin
|
||||
FOR process_rec in
|
||||
SELECT * FROM process_queue
|
||||
WHERE channel = 'new_logbook' AND processed IS NULL
|
||||
ORDER BY stored ASC
|
||||
ORDER BY stored ASC LIMIT 100
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_new_logbook_fn [%]', process_rec.payload;
|
||||
-- update logbook
|
||||
@@ -47,7 +47,7 @@ begin
|
||||
FOR process_rec in
|
||||
SELECT * FROM process_queue
|
||||
WHERE channel = 'new_stay' AND processed IS NULL
|
||||
ORDER BY stored ASC
|
||||
ORDER BY stored ASC LIMIT 100
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_new_stay_fn [%]', process_rec.payload;
|
||||
-- update stay
|
||||
@@ -77,7 +77,7 @@ begin
|
||||
FOR process_rec in
|
||||
SELECT * FROM process_queue
|
||||
WHERE channel = 'new_moorage' AND processed IS NULL
|
||||
ORDER BY stored ASC
|
||||
ORDER BY stored ASC LIMIT 100
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_new_moorage_fn [%]', process_rec.payload;
|
||||
-- update moorage
|
||||
@@ -124,30 +124,30 @@ begin
|
||||
active = False
|
||||
WHERE id = metadata_rec.id;
|
||||
|
||||
IF metadata_rec.client_id IS NULL OR metadata_rec.client_id = '' THEN
|
||||
RAISE WARNING '-> cron_process_monitor_offline_fn invalid metadata record client_id %', client_id;
|
||||
IF metadata_rec.vessel_id IS NULL OR metadata_rec.vessel_id = '' THEN
|
||||
RAISE WARNING '-> cron_process_monitor_offline_fn invalid metadata record vessel_id %', vessel_id;
|
||||
RAISE EXCEPTION 'Invalid metadata'
|
||||
USING HINT = 'Unkown client_id';
|
||||
USING HINT = 'Unknow vessel_id';
|
||||
RETURN;
|
||||
END IF;
|
||||
PERFORM set_config('vessel.client_id', metadata_rec.client_id, false);
|
||||
RAISE DEBUG '-> DEBUG cron_process_monitor_offline_fn vessel.client_id %', current_setting('vessel.client_id', false);
|
||||
RAISE NOTICE '-> cron_process_monitor_offline_fn updated api.metadata table to inactive for [%] [%]', metadata_rec.id, metadata_rec.client_id;
|
||||
PERFORM set_config('vessel.id', metadata_rec.vessel_id, false);
|
||||
RAISE DEBUG '-> DEBUG cron_process_monitor_offline_fn vessel.id %', current_setting('vessel.id', false);
|
||||
RAISE NOTICE '-> cron_process_monitor_offline_fn updated api.metadata table to inactive for [%] [%]', metadata_rec.id, metadata_rec.vessel_id;
|
||||
|
||||
-- Gather email and pushover app settings
|
||||
--app_settings = get_app_settings_fn();
|
||||
-- Gather user settings
|
||||
user_settings := get_user_settings_from_clientid_fn(metadata_rec.client_id::TEXT);
|
||||
RAISE DEBUG '-> cron_process_monitor_offline_fn get_user_settings_from_clientid_fn [%]', user_settings;
|
||||
user_settings := get_user_settings_from_vesselid_fn(metadata_rec.vessel_id::TEXT);
|
||||
RAISE DEBUG '-> cron_process_monitor_offline_fn get_user_settings_from_vesselid_fn [%]', user_settings;
|
||||
-- Send notification
|
||||
PERFORM send_notification_fn('monitor_offline'::TEXT, user_settings::JSONB);
|
||||
--PERFORM send_email_py_fn('monitor_offline'::TEXT, user_settings::JSONB, app_settings::JSONB);
|
||||
--PERFORM send_pushover_py_fn('monitor_offline'::TEXT, user_settings::JSONB, app_settings::JSONB);
|
||||
-- log/insert/update process_queue table with processed
|
||||
INSERT INTO process_queue
|
||||
(channel, payload, stored, processed)
|
||||
(channel, payload, stored, processed, ref_id)
|
||||
VALUES
|
||||
('monitoring_offline', metadata_rec.id, metadata_rec.interval, now())
|
||||
('monitoring_offline', metadata_rec.id, metadata_rec.interval, now(), metadata_rec.vessel_id)
|
||||
RETURNING id INTO process_id;
|
||||
RAISE NOTICE '-> cron_process_monitor_offline_fn updated process_queue table [%]', process_id;
|
||||
END LOOP;
|
||||
@@ -179,20 +179,20 @@ begin
|
||||
FROM api.metadata
|
||||
WHERE id = process_rec.payload::INTEGER;
|
||||
|
||||
IF metadata_rec.client_id IS NULL OR metadata_rec.client_id = '' THEN
|
||||
RAISE WARNING '-> cron_process_monitor_online_fn invalid metadata record client_id %', client_id;
|
||||
IF metadata_rec.vessel_id IS NULL OR metadata_rec.vessel_id = '' THEN
|
||||
RAISE WARNING '-> cron_process_monitor_online_fn invalid metadata record vessel_id %', vessel_id;
|
||||
RAISE EXCEPTION 'Invalid metadata'
|
||||
USING HINT = 'Unkown client_id';
|
||||
USING HINT = 'Unknow vessel_id';
|
||||
RETURN;
|
||||
END IF;
|
||||
PERFORM set_config('vessel.client_id', metadata_rec.client_id, false);
|
||||
RAISE DEBUG '-> DEBUG cron_process_monitor_online_fn vessel.client_id %', current_setting('vessel.client_id', false);
|
||||
PERFORM set_config('vessel.id', metadata_rec.vessel_id, false);
|
||||
RAISE DEBUG '-> DEBUG cron_process_monitor_online_fn vessel_id %', current_setting('vessel.id', false);
|
||||
|
||||
-- Gather email and pushover app settings
|
||||
--app_settings = get_app_settings_fn();
|
||||
-- Gather user settings
|
||||
user_settings := get_user_settings_from_clientid_fn(metadata_rec.client_id::TEXT);
|
||||
RAISE DEBUG '-> DEBUG cron_process_monitor_online_fn get_user_settings_from_clientid_fn [%]', user_settings;
|
||||
user_settings := get_user_settings_from_vesselid_fn(metadata_rec.vessel_id::TEXT);
|
||||
RAISE DEBUG '-> DEBUG cron_process_monitor_online_fn get_user_settings_from_vesselid_fn [%]', user_settings;
|
||||
-- Send notification
|
||||
PERFORM send_notification_fn('monitor_online'::TEXT, user_settings::JSONB);
|
||||
--PERFORM send_email_py_fn('monitor_online'::TEXT, user_settings::JSONB, app_settings::JSONB);
|
||||
@@ -238,7 +238,7 @@ $$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.cron_process_new_account_fn
|
||||
IS 'init by pg_cron to check for new account pending update, if so perform process_account_queue_fn';
|
||||
IS 'deprecated, init by pg_cron to check for new account pending update, if so perform process_account_queue_fn';
|
||||
|
||||
-- CRON for new account pending otp validation notification
|
||||
CREATE FUNCTION cron_process_new_account_otp_validation_fn() RETURNS void AS $$
|
||||
@@ -267,7 +267,7 @@ $$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.cron_process_new_account_otp_validation_fn
|
||||
IS 'init by pg_cron to check for new account otp pending update, if so perform process_account_otp_validation_queue_fn';
|
||||
IS 'deprecated, init by pg_cron to check for new account otp pending update, if so perform process_account_otp_validation_queue_fn';
|
||||
|
||||
-- CRON for new vessel pending notification
|
||||
CREATE FUNCTION cron_process_new_vessel_fn() RETURNS void AS $$
|
||||
@@ -296,7 +296,7 @@ $$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.cron_process_new_vessel_fn
|
||||
IS 'init by pg_cron to check for new vessel pending update, if so perform process_vessel_queue_fn';
|
||||
IS 'deprecated, init by pg_cron to check for new vessel pending update, if so perform process_vessel_queue_fn';
|
||||
|
||||
-- CRON for new event notification
|
||||
CREATE FUNCTION cron_process_new_notification_fn() RETURNS void AS $$
|
||||
@@ -330,12 +330,12 @@ COMMENT ON FUNCTION
|
||||
IS 'init by pg_cron to check for new event pending notifications, if so perform process_notification_queue_fn';
|
||||
|
||||
-- CRON for Vacuum database
|
||||
CREATE FUNCTION cron_vaccum_fn() RETURNS void AS $$
|
||||
CREATE FUNCTION cron_vacuum_fn() RETURNS void AS $$
|
||||
-- ERROR: VACUUM cannot be executed from a function
|
||||
declare
|
||||
begin
|
||||
-- Vacuum
|
||||
RAISE NOTICE 'cron_vaccum_fn';
|
||||
RAISE NOTICE 'cron_vacuum_fn';
|
||||
VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) api.logbook;
|
||||
VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) api.stays;
|
||||
VACUUM (FULL, VERBOSE, ANALYZE, INDEX_CLEANUP) api.moorages;
|
||||
@@ -345,5 +345,44 @@ END;
|
||||
$$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.cron_vaccum_fn
|
||||
IS 'init by pg_cron to full vaccum tables on schema api';
|
||||
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;
|
||||
BEGIN
|
||||
-- Check for new event notification pending update
|
||||
RAISE NOTICE 'cron_process_alerts_fn';
|
||||
FOR alert_rec in
|
||||
SELECT
|
||||
a.user_id,a.email,v.vessel_id
|
||||
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
|
||||
LOOP
|
||||
RAISE NOTICE '-> cron_process_alert_rec_fn for [%]', alert_rec;
|
||||
END LOOP;
|
||||
END;
|
||||
$$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.cron_process_alerts_fn
|
||||
IS 'init by pg_cron to check for alerts, if so perform process_alerts_queue_fn';
|
||||
|
136
initdb/02_3_3_signalk_public_functions_helpers.sql
Normal file
@@ -0,0 +1,136 @@
|
||||
---------------------------------------------------------------------------
|
||||
-- singalk db public schema
|
||||
--
|
||||
|
||||
-- List current database
|
||||
select current_database();
|
||||
|
||||
-- connect to the DB
|
||||
\c signalk
|
||||
|
||||
CREATE SCHEMA IF NOT EXISTS public;
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- basic helpers to check type and more
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION public.isnumeric(text) RETURNS BOOLEAN AS
|
||||
$isnumeric$
|
||||
DECLARE x NUMERIC;
|
||||
BEGIN
|
||||
x = $1::NUMERIC;
|
||||
RETURN TRUE;
|
||||
EXCEPTION WHEN others THEN
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$isnumeric$
|
||||
STRICT
|
||||
LANGUAGE plpgsql IMMUTABLE;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.isnumeric
|
||||
IS 'Check typeof value is numeric';
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.isboolean(text) RETURNS BOOLEAN AS
|
||||
$isboolean$
|
||||
DECLARE x BOOLEAN;
|
||||
BEGIN
|
||||
x = $1::BOOLEAN;
|
||||
RETURN TRUE;
|
||||
EXCEPTION WHEN others THEN
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$isboolean$
|
||||
STRICT
|
||||
LANGUAGE plpgsql IMMUTABLE;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.isboolean
|
||||
IS 'Check typeof value is boolean';
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.isdate(s varchar) returns boolean as $$
|
||||
BEGIN
|
||||
perform s::date;
|
||||
return true;
|
||||
exception when others then
|
||||
return false;
|
||||
END;
|
||||
$$ language plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.isdate
|
||||
IS 'Check typeof value is date';
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.istimestamptz(text) RETURNS BOOLEAN AS
|
||||
$isdate$
|
||||
DECLARE x TIMESTAMP WITHOUT TIME ZONE;
|
||||
BEGIN
|
||||
x = $1::TIMESTAMP WITHOUT TIME ZONE;
|
||||
RETURN TRUE;
|
||||
EXCEPTION WHEN others THEN
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$isdate$
|
||||
STRICT
|
||||
LANGUAGE plpgsql IMMUTABLE;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.istimestamptz
|
||||
IS 'Check typeof value is TIMESTAMP WITHOUT TIME ZONE';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- JSON helpers
|
||||
--
|
||||
CREATE FUNCTION jsonb_key_exists(some_json jsonb, outer_key text)
|
||||
RETURNS BOOLEAN AS $$
|
||||
BEGIN
|
||||
RETURN (some_json->outer_key) IS NOT NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.jsonb_key_exists
|
||||
IS 'function that checks if an outer key exists in some_json and returns a boolean';
|
||||
|
||||
-- https://stackoverflow.com/questions/42944888/merging-jsonb-values-in-postgresql
|
||||
CREATE OR REPLACE FUNCTION public.jsonb_recursive_merge(A jsonb, B jsonb)
|
||||
RETURNS jsonb LANGUAGE SQL AS $$
|
||||
SELECT
|
||||
jsonb_object_agg(
|
||||
coalesce(ka, kb),
|
||||
CASE
|
||||
WHEN va isnull THEN vb
|
||||
WHEN vb isnull THEN va
|
||||
WHEN jsonb_typeof(va) <> 'object' OR jsonb_typeof(vb) <> 'object' THEN vb
|
||||
ELSE jsonb_recursive_merge(va, vb) END
|
||||
)
|
||||
FROM jsonb_each(A) temptable1(ka, va)
|
||||
FULL JOIN jsonb_each(B) temptable2(kb, vb) ON ka = kb
|
||||
$$;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.jsonb_recursive_merge
|
||||
IS 'Merging JSONB values';
|
||||
|
||||
-- https://stackoverflow.com/questions/36041784/postgresql-compare-two-jsonb-objects
|
||||
CREATE OR REPLACE FUNCTION public.jsonb_diff_val(val1 JSONB,val2 JSONB)
|
||||
RETURNS JSONB AS $jsonb_diff_val$
|
||||
DECLARE
|
||||
result JSONB;
|
||||
v RECORD;
|
||||
BEGIN
|
||||
result = val1;
|
||||
FOR v IN SELECT * FROM jsonb_each(val2) LOOP
|
||||
IF result @> jsonb_build_object(v.key,v.value)
|
||||
THEN result = result - v.key;
|
||||
ELSIF result ? v.key THEN CONTINUE;
|
||||
ELSE
|
||||
result = result || jsonb_build_object(v.key,'null');
|
||||
END IF;
|
||||
END LOOP;
|
||||
RETURN result;
|
||||
END;
|
||||
$jsonb_diff_val$ LANGUAGE plpgsql;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.jsonb_diff_val
|
||||
IS 'Compare two jsonb objects';
|
@@ -51,14 +51,26 @@ AS $reverse_geocode_py$
|
||||
r_dict = r.json()
|
||||
if r_dict["name"]:
|
||||
return r_dict["name"]
|
||||
elif "address" in r_dict and r_dict["address"] and r_dict["address"]["road"]:
|
||||
elif "address" in r_dict and r_dict["address"]:
|
||||
if "road" in r_dict["address"] and r_dict["address"]["road"]:
|
||||
return r_dict["address"]["road"]
|
||||
elif "address" in r_dict and r_dict["address"] and r_dict["address"]["neighbourhood"]:
|
||||
elif "neighbourhood" in r_dict["address"] and r_dict["address"]["neighbourhood"]:
|
||||
return r_dict["address"]["neighbourhood"]
|
||||
elif "address" in r_dict and r_dict["address"] and r_dict["address"]["suburb"]:
|
||||
elif "suburb" in r_dict["address"] and r_dict["address"]["suburb"]:
|
||||
return r_dict["address"]["suburb"]
|
||||
elif "residential" in r_dict["address"] and r_dict["address"]["residential"]:
|
||||
return r_dict["address"]["residential"]
|
||||
elif "village" in r_dict["address"] and r_dict["address"]["village"]:
|
||||
return r_dict["address"]["village"]
|
||||
elif "town" in r_dict["address"] and r_dict["address"]["town"]:
|
||||
return r_dict["address"]["town"]
|
||||
else:
|
||||
plpy.error('Failed to received a geo full address %s', r.json())
|
||||
return 'n/a'
|
||||
else:
|
||||
return 'n/a'
|
||||
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 'unknow'
|
||||
$reverse_geocode_py$ LANGUAGE plpython3u;
|
||||
-- Description
|
||||
@@ -333,10 +345,10 @@ $urlencode_py$ LANGUAGE plpython3u IMMUTABLE STRICT;
|
||||
-- python
|
||||
-- https://ipapi.co/
|
||||
DROP FUNCTION IF EXISTS reverse_geoip_py_fn;
|
||||
CREATE OR REPLACE FUNCTION reverse_geoip_py_fn(IN _ip TEXT) RETURNS void
|
||||
CREATE OR REPLACE FUNCTION reverse_geoip_py_fn(IN _ip TEXT) RETURNS JSONB
|
||||
AS $reverse_geoip_py$
|
||||
"""
|
||||
TODO
|
||||
Return ipapi.co ip details
|
||||
"""
|
||||
import requests
|
||||
import json
|
||||
@@ -346,14 +358,61 @@ AS $reverse_geoip_py$
|
||||
r = requests.get(url)
|
||||
#print(r.text)
|
||||
# Return something boolean?
|
||||
#plpy.notice('Sent successfully to [{}] [{}]'.format(r.text, r.status_code))
|
||||
#plpy.notice('IP [{}] [{}]'.format(_ip, r.status_code))
|
||||
if r.status_code == 200:
|
||||
plpy.notice('Sent successfully to [{}] [{}]'.format(r.text, r.status_code))
|
||||
#plpy.notice('Got [{}] [{}]'.format(r.text, r.status_code))
|
||||
return r.text;
|
||||
else:
|
||||
plpy.error('Failed to send')
|
||||
return None
|
||||
$reverse_geoip_py$ TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
|
||||
plpy.error('Failed to get ip details')
|
||||
return '{}'
|
||||
$reverse_geoip_py$ LANGUAGE plpython3u;
|
||||
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.reverse_geoip_py_fn
|
||||
IS 'Retrieve reverse geo IP location via ipapi.co using plpython3u';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- python url escape
|
||||
--
|
||||
DROP FUNCTION IF EXISTS urlescape_py_fn;
|
||||
CREATE OR REPLACE FUNCTION urlescape_py_fn(original text) RETURNS text LANGUAGE plpython3u AS $$
|
||||
import urllib.parse
|
||||
return urllib.parse.quote(original);
|
||||
$$
|
||||
IMMUTABLE STRICT;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.urlescape_py_fn
|
||||
IS 'URL-encoding VARCHAR and TEXT values using plpython3u';
|
||||
|
||||
---------------------------------------------------------------------------
|
||||
-- python geojson parser
|
||||
--
|
||||
--CREATE TYPE geometry_type AS ENUM ('LineString', 'Point');
|
||||
DROP FUNCTION IF EXISTS geojson_py_fn;
|
||||
CREATE OR REPLACE FUNCTION geojson_py_fn(IN original JSONB, IN geometry_type TEXT) RETURNS JSONB LANGUAGE plpython3u
|
||||
AS $geojson_py$
|
||||
import json
|
||||
parsed = json.loads(original)
|
||||
output = []
|
||||
#plpy.notice(parsed)
|
||||
# [None, None]
|
||||
if None not in parsed:
|
||||
for idx, x in enumerate(parsed):
|
||||
#plpy.notice(idx, x)
|
||||
for feature in x:
|
||||
#plpy.notice(feature)
|
||||
if (feature['geometry']['type'] != geometry_type):
|
||||
output.append(feature)
|
||||
#elif (feature['properties']['id']): TODO
|
||||
# output.append(feature)
|
||||
#else:
|
||||
# plpy.notice('ignoring')
|
||||
return json.dumps(output)
|
||||
$geojson_py$ -- TRANSFORM FOR TYPE jsonb LANGUAGE plpython3u;
|
||||
IMMUTABLE STRICT;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.geojson_py_fn
|
||||
IS 'Parse geojson using plpython3u (should be done in PGSQL)';
|
||||
|
@@ -42,9 +42,11 @@ COMMENT ON TABLE
|
||||
auth.accounts
|
||||
IS 'users account table';
|
||||
-- Indexes
|
||||
CREATE INDEX accounts_role_idx ON auth.accounts (role);
|
||||
-- is unused index?
|
||||
--CREATE INDEX accounts_role_idx ON auth.accounts (role);
|
||||
CREATE INDEX accounts_preferences_idx ON auth.accounts using GIN (preferences);
|
||||
CREATE INDEX accounts_userid_idx ON auth.accounts (userid);
|
||||
-- is unused index?
|
||||
--CREATE INDEX accounts_userid_idx ON auth.accounts (userid);
|
||||
|
||||
CREATE TRIGGER accounts_moddatetime
|
||||
BEFORE UPDATE ON auth.accounts
|
||||
@@ -58,7 +60,7 @@ 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),
|
||||
-- user_id REFERENCES auth.accounts(user_id) ON DELETE RESTRICT,
|
||||
-- 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
|
||||
@@ -73,10 +75,12 @@ CREATE TABLE IF NOT EXISTS auth.vessels (
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
auth.vessels
|
||||
IS 'vessels table link to accounts email column';
|
||||
IS 'vessels table link to accounts email user_id column';
|
||||
-- Indexes
|
||||
CREATE INDEX vessels_role_idx ON auth.vessels (role);
|
||||
CREATE INDEX vessels_name_idx ON auth.vessels (name);
|
||||
-- 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);
|
||||
|
||||
CREATE TRIGGER vessels_moddatetime
|
||||
@@ -172,6 +176,9 @@ declare
|
||||
_role name;
|
||||
result auth.jwt_token;
|
||||
app_jwt_secret text;
|
||||
_email_valid boolean := false;
|
||||
_email text := email;
|
||||
_user_id text := null;
|
||||
begin
|
||||
-- check email and password
|
||||
select auth.user_role(email, pass) into _role;
|
||||
@@ -184,13 +191,25 @@ begin
|
||||
FROM app_settings
|
||||
WHERE name = 'app.jwt_secret';
|
||||
|
||||
-- Check email_valid and generate OTP
|
||||
SELECT preferences['email_valid'],user_id INTO _email_valid,_user_id
|
||||
FROM auth.accounts a
|
||||
WHERE a.email = _email;
|
||||
IF _email_valid is null or _email_valid is False THEN
|
||||
INSERT INTO process_queue (channel, payload, stored, ref_id)
|
||||
VALUES ('email_otp', email, now(), _user_id);
|
||||
END IF;
|
||||
|
||||
--RAISE WARNING 'api.login debug: [%],[%],[%]', app_jwt_secret, _role, login.email;
|
||||
-- Generate jwt
|
||||
select jwt.sign(
|
||||
-- row_to_json(r), ''
|
||||
-- row_to_json(r)::json, current_setting('app.jwt_secret')::text
|
||||
row_to_json(r)::json, app_jwt_secret
|
||||
) as token
|
||||
from (
|
||||
select _role as role, login.email as email,
|
||||
select _role as role, login.email as email, -- TODO replace with user_id
|
||||
-- select _role as role, user_id as uid, -- add support in check_jwt
|
||||
extract(epoch from now())::integer + 60*60 as exp
|
||||
) r
|
||||
into result;
|
||||
@@ -263,7 +282,8 @@ begin
|
||||
) as token
|
||||
from (
|
||||
select vessel_rec.role as role,
|
||||
vessel_rec.owner_email as email,
|
||||
vessel_rec.owner_email as email, -- TODO replace with user_id
|
||||
-- vessel_rec.user_id as uid
|
||||
vessel_rec.vessel_id as vid
|
||||
) r
|
||||
into result;
|
||||
|
@@ -9,8 +9,18 @@ select current_database();
|
||||
\c signalk
|
||||
|
||||
-- Link auth.vessels with api.metadata
|
||||
ALTER TABLE api.metadata ADD vessel_id TEXT NOT NULL REFERENCES auth.vessels(vessel_id) ON DELETE RESTRICT;
|
||||
COMMENT ON COLUMN api.metadata.vessel_id IS 'Link auth.vessels with api.metadata';
|
||||
--ALTER TABLE api.metadata ADD vessel_id TEXT NOT NULL REFERENCES auth.vessels(vessel_id) ON DELETE RESTRICT;
|
||||
ALTER TABLE api.metadata ADD FOREIGN KEY (vessel_id) REFERENCES auth.vessels(vessel_id) ON DELETE RESTRICT;
|
||||
COMMENT ON COLUMN api.metadata.vessel_id IS 'Link auth.vessels with api.metadata via FOREIGN KEY and REFERENCES';
|
||||
|
||||
-- Link auth.vessels with auth.accounts
|
||||
--ALTER TABLE auth.vessels ADD user_id TEXT NOT NULL REFERENCES auth.accounts(user_id) ON DELETE RESTRICT;
|
||||
--COMMENT ON COLUMN auth.vessels.user_id IS 'Link auth.vessels with auth.accounts';
|
||||
--COMMENT ON COLUMN auth.vessels.vessel_id IS 'Vessel identifier. Link auth.vessels with api.metadata';
|
||||
|
||||
-- REFERENCE ship type with AIS type ?
|
||||
-- REFERENCE mmsi MID with country ?
|
||||
|
||||
|
||||
-- List vessel
|
||||
--TODO add geojson with position
|
||||
@@ -22,7 +32,7 @@ CREATE OR REPLACE VIEW api.vessels_view AS
|
||||
FROM api.metadata m
|
||||
WHERE m.vessel_id = current_setting('vessel.id')
|
||||
)::TEXT ,
|
||||
''::TEXT ) as last_contact
|
||||
NULL ) as last_contact
|
||||
)
|
||||
SELECT
|
||||
v.name as name,
|
||||
@@ -31,27 +41,49 @@ CREATE OR REPLACE VIEW api.vessels_view AS
|
||||
m.last_contact as last_contact
|
||||
FROM auth.vessels v, metadata m
|
||||
WHERE v.owner_email = current_setting('user.email');
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
api.vessels_view
|
||||
IS 'Expose vessels listing to web api';
|
||||
|
||||
CREATE OR REPLACE VIEW api.vessels2_view AS
|
||||
-- TODO
|
||||
SELECT
|
||||
v.name as name,
|
||||
v.mmsi as mmsi,
|
||||
v.created_at::timestamp(0) as created_at,
|
||||
COALESCE(m.time, null) as last_contact
|
||||
FROM auth.vessels v
|
||||
LEFT JOIN api.metadata m ON v.owner_email = current_setting('user.email')
|
||||
AND m.vessel_id = current_setting('vessel.id');
|
||||
DROP FUNCTION IF EXISTS public.has_vessel_fn;
|
||||
CREATE OR REPLACE FUNCTION public.has_vessel_fn() RETURNS BOOLEAN
|
||||
AS $has_vessel$
|
||||
DECLARE
|
||||
BEGIN
|
||||
-- Check a vessel and user exist
|
||||
RETURN (
|
||||
SELECT auth.vessels.name
|
||||
FROM auth.vessels, auth.accounts
|
||||
WHERE auth.vessels.owner_email = auth.accounts.email
|
||||
AND auth.accounts.email = current_setting('user.email')
|
||||
) IS NOT NULL;
|
||||
END;
|
||||
$has_vessel$ language plpgsql security definer;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.has_vessel_fn
|
||||
IS 'Check if user has a vessel register';
|
||||
|
||||
DROP VIEW IF EXISTS api.vessel_p_view;
|
||||
CREATE OR REPLACE VIEW api.vessel_p_view AS
|
||||
SELECT
|
||||
v.name as name,
|
||||
v.mmsi as mmsi,
|
||||
v.created_at::timestamp(0) as created_at,
|
||||
null as last_contact
|
||||
FROM auth.vessels v
|
||||
WHERE v.owner_email = current_setting('user.email');
|
||||
DROP FUNCTION IF EXISTS public.has_vessel_metadata_fn;
|
||||
CREATE OR REPLACE FUNCTION public.has_vessel_metadata_fn() RETURNS BOOLEAN
|
||||
AS $has_vessel_metadata$
|
||||
DECLARE
|
||||
BEGIN
|
||||
-- Check a vessel metadata
|
||||
RETURN (
|
||||
SELECT m.vessel_id
|
||||
FROM auth.accounts a, auth.vessels v, api.metadata m
|
||||
WHERE m.vessel_id = v.vessel_id
|
||||
AND auth.vessels.owner_email = auth.accounts.email
|
||||
AND auth.accounts.email = current_setting('user.email')
|
||||
) IS NOT NULL;
|
||||
END;
|
||||
$has_vessel_metadata$ language plpgsql security definer;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.has_vessel_metadata_fn
|
||||
IS 'Check if user has a vessel register';
|
||||
|
||||
-- Or function?
|
||||
-- TODO Improve: return null until the vessel has sent metadata?
|
||||
@@ -61,18 +93,15 @@ AS $vessel$
|
||||
DECLARE
|
||||
BEGIN
|
||||
SELECT
|
||||
json_build_object(
|
||||
jsonb_build_object(
|
||||
'name', v.name,
|
||||
'mmsi', coalesce(v.mmsi, null),
|
||||
'created_at', v.created_at::timestamp(0),
|
||||
'last_contact', coalesce(m.time, null),
|
||||
'geojson', coalesce(ST_AsGeoJSON(geojson_t.*)::json, null)
|
||||
)
|
||||
)::jsonb || api.vessel_details_fn()::jsonb
|
||||
INTO vessel
|
||||
FROM auth.vessels v, api.metadata m,
|
||||
( SELECT
|
||||
t.*
|
||||
FROM (
|
||||
( select
|
||||
current_setting('vessel.name') as name,
|
||||
time,
|
||||
@@ -85,9 +114,8 @@ AS $vessel$
|
||||
WHERE
|
||||
latitude IS NOT NULL
|
||||
AND longitude IS NOT NULL
|
||||
AND client_id = current_setting('vessel.client_id', false)
|
||||
)
|
||||
) AS t
|
||||
AND vessel_id = current_setting('vessel.id', false)
|
||||
ORDER BY time DESC
|
||||
) AS geojson_t
|
||||
WHERE
|
||||
m.vessel_id = current_setting('vessel.id')
|
||||
@@ -102,14 +130,16 @@ COMMENT ON FUNCTION
|
||||
|
||||
-- Export user settings
|
||||
DROP FUNCTION IF EXISTS api.settings_fn;
|
||||
CREATE FUNCTION api.settings_fn(out settings json) RETURNS JSON
|
||||
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 email,first,last,preferences,created_at,
|
||||
INITCAP(CONCAT (LEFT(first, 1), ' ', last)) AS username
|
||||
from auth.accounts
|
||||
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;
|
||||
END;
|
||||
@@ -126,12 +156,15 @@ AS $version$
|
||||
_appv TEXT;
|
||||
_sysv TEXT;
|
||||
BEGIN
|
||||
-- Add postgrest version https://postgrest.org/en/v11.2/references/admin.html#server-version
|
||||
SELECT
|
||||
value, version() into _appv,_sysv
|
||||
value, rtrim(substring(version(), 0, 17)) AS sys_version into _appv,_sysv
|
||||
FROM app_settings
|
||||
WHERE name = 'app.version';
|
||||
RETURN json_build_object('app_version', _appv,
|
||||
'sys_version', _sysv);
|
||||
RETURN json_build_object('api_version', _appv,
|
||||
'sys_version', _sysv,
|
||||
'timescaledb', (SELECT extversion as timescaledb FROM pg_extension WHERE extname='timescaledb'),
|
||||
'postgis', (SELECT extversion as postgis FROM pg_extension WHERE extname='postgis'));
|
||||
END;
|
||||
$version$ language plpgsql security definer;
|
||||
-- Description
|
||||
@@ -141,9 +174,13 @@ COMMENT ON FUNCTION
|
||||
|
||||
DROP VIEW IF EXISTS api.versions_view;
|
||||
CREATE OR REPLACE VIEW api.versions_view AS
|
||||
-- Add postgrest version https://postgrest.org/en/v11.2/references/admin.html#server-version
|
||||
SELECT
|
||||
value as app_version,
|
||||
version() as sys_version
|
||||
value AS api_version,
|
||||
--version() as sys_version
|
||||
rtrim(substring(version(), 0, 17)) AS sys_version,
|
||||
(SELECT extversion as timescaledb FROM pg_extension WHERE extname='timescaledb'),
|
||||
(SELECT extversion as postgis FROM pg_extension WHERE extname='postgis')
|
||||
FROM app_settings
|
||||
WHERE name = 'app.version';
|
||||
-- Description
|
||||
@@ -151,40 +188,6 @@ COMMENT ON VIEW
|
||||
api.versions_view
|
||||
IS 'Expose as a table view app and system version to API';
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.isnumeric(text) RETURNS BOOLEAN AS
|
||||
$isnumeric$
|
||||
DECLARE x NUMERIC;
|
||||
BEGIN
|
||||
x = $1::NUMERIC;
|
||||
RETURN TRUE;
|
||||
EXCEPTION WHEN others THEN
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$isnumeric$
|
||||
STRICT
|
||||
LANGUAGE plpgsql IMMUTABLE;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.isnumeric
|
||||
IS 'Check typeof value is numeric';
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.isboolean(text) RETURNS BOOLEAN AS
|
||||
$isboolean$
|
||||
DECLARE x BOOLEAN;
|
||||
BEGIN
|
||||
x = $1::BOOLEAN;
|
||||
RETURN TRUE;
|
||||
EXCEPTION WHEN others THEN
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$isboolean$
|
||||
STRICT
|
||||
LANGUAGE plpgsql IMMUTABLE;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
public.isboolean
|
||||
IS 'Check typeof value is boolean';
|
||||
|
||||
DROP FUNCTION IF EXISTS api.update_user_preferences_fn;
|
||||
-- Update/Add a specific user setting into preferences
|
||||
CREATE OR REPLACE FUNCTION api.update_user_preferences_fn(IN key TEXT, IN value TEXT) RETURNS BOOLEAN AS
|
||||
@@ -220,3 +223,39 @@ $update_user_preferences$ language plpgsql security definer;
|
||||
COMMENT ON FUNCTION
|
||||
api.update_user_preferences_fn
|
||||
IS 'Update user preferences jsonb key pair value';
|
||||
|
||||
DROP FUNCTION IF EXISTS api.vessel_details_fn;
|
||||
CREATE OR REPLACE FUNCTION api.vessel_details_fn() RETURNS JSON AS
|
||||
$vessel_details$
|
||||
DECLARE
|
||||
BEGIN
|
||||
RETURN ( WITH tbl AS (
|
||||
SELECT mmsi,ship_type,length,beam,height FROM api.metadata WHERE vessel_id = current_setting('vessel.id', false)
|
||||
)
|
||||
SELECT json_build_object(
|
||||
'ship_type', (SELECT ais.description FROM aistypes ais, tbl WHERE t.ship_type = ais.id),
|
||||
'country', (SELECT mid.country FROM mid, tbl WHERE LEFT(cast(mmsi as text), 3)::NUMERIC = mid.id),
|
||||
'alpha_2', (SELECT o.alpha_2 FROM mid m, iso3166 o, tbl WHERE LEFT(cast(mmsi as text), 3)::NUMERIC = m.id AND m.country_id = o.id),
|
||||
'length', t.ship_type,
|
||||
'beam', t.beam,
|
||||
'height', t.height)
|
||||
FROM tbl t
|
||||
);
|
||||
END;
|
||||
$vessel_details$ language plpgsql security definer;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.vessel_details_fn
|
||||
IS 'Return vessel details such as metadata (length,beam,height), ais type and country name and country iso3166-alpha-2';
|
||||
|
||||
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;
|
||||
-- Description
|
||||
COMMENT ON VIEW
|
||||
api.eventlogs_view
|
||||
IS 'Event logs view';
|
@@ -54,7 +54,7 @@ AS $generate_otp$
|
||||
DECLARE
|
||||
_email CITEXT := email;
|
||||
_email_check TEXT := NULL;
|
||||
otp_pass VARCHAR(10) := NULL;
|
||||
_otp_pass VARCHAR(10) := NULL;
|
||||
BEGIN
|
||||
IF email IS NULL OR _email IS NULL OR _email = '' THEN
|
||||
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
|
||||
@@ -64,9 +64,12 @@ AS $generate_otp$
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
--SELECT substr(gen_random_uuid()::text, 1, 6) INTO otp_pass;
|
||||
SELECT generate_uid_fn(6) INTO otp_pass;
|
||||
INSERT INTO auth.otp (user_email, otp_pass) VALUES (_email_check, otp_pass);
|
||||
RETURN otp_pass;
|
||||
SELECT generate_uid_fn(6) INTO _otp_pass;
|
||||
-- upsert - Insert or update otp code on conflit
|
||||
INSERT INTO auth.otp (user_email, otp_pass)
|
||||
VALUES (_email_check, _otp_pass)
|
||||
ON CONFLICT (user_email) DO UPDATE SET otp_pass = _otp_pass, otp_timestamp = NOW();
|
||||
RETURN _otp_pass;
|
||||
END;
|
||||
$generate_otp$ language plpgsql security definer;
|
||||
-- Description
|
||||
@@ -80,7 +83,7 @@ AS $recover_fn$
|
||||
DECLARE
|
||||
_email CITEXT := email;
|
||||
_user_id TEXT := NULL;
|
||||
otp_pass VARCHAR(10) := NULL;
|
||||
otp_pass TEXT := NULL;
|
||||
_reset_qs TEXT := NULL;
|
||||
user_settings jsonb := NULL;
|
||||
BEGIN
|
||||
@@ -93,10 +96,11 @@ AS $recover_fn$
|
||||
RAISE EXCEPTION 'Invalid input'
|
||||
USING HINT = 'Check your parameter';
|
||||
END IF;
|
||||
-- OTP Code
|
||||
SELECT generate_uid_fn(6) INTO otp_pass;
|
||||
INSERT INTO auth.otp (user_email, otp_pass) VALUES (_email, otp_pass);
|
||||
-- Generate OTP
|
||||
otp_pass := api.generate_otp_fn(email);
|
||||
SELECT CONCAT('uuid=', _user_id, '&token=', otp_pass) INTO _reset_qs;
|
||||
-- Enable email_notifications
|
||||
PERFORM api.update_user_preferences_fn('{email_notifications}'::TEXT, True::TEXT);
|
||||
-- Send email/notifications
|
||||
user_settings := '{"email": "' || _email || '", "reset_qs": "' || _reset_qs || '"}';
|
||||
PERFORM send_notification_fn('email_reset'::TEXT, user_settings::JSONB);
|
||||
@@ -239,7 +243,11 @@ COMMENT ON FUNCTION
|
||||
api.email_fn
|
||||
IS 'Store email_valid into user preferences if valid token/otp';
|
||||
|
||||
CREATE OR REPLACE FUNCTION api.pushover_subscribe_link_fn(IN email TEXT, OUT pushover_link JSON) RETURNS JSON
|
||||
-- Pushover Subscription API
|
||||
-- Web-Based Subscription Process
|
||||
-- https://pushover.net/api/subscriptions#web
|
||||
-- Expose as an API endpoint
|
||||
CREATE OR REPLACE FUNCTION api.pushover_subscribe_link_fn(OUT pushover_link JSON) RETURNS JSON
|
||||
AS $pushover_subscribe_link$
|
||||
DECLARE
|
||||
app_url text;
|
||||
@@ -247,11 +255,12 @@ AS $pushover_subscribe_link$
|
||||
pushover_app_url text;
|
||||
success text;
|
||||
failure text;
|
||||
email text := current_setting('user.email', true);
|
||||
BEGIN
|
||||
--https://pushover.net/api/subscriptions#web
|
||||
-- "https://pushover.net/subscribe/PostgSail-23uvrho1d5y6n3e"
|
||||
-- + "?success=" + urlencode("https://beta.openplotter.cloud/api/rpc/pushover_fn?token=" + generate_otp_fn({{email}}))
|
||||
-- + "&failure=" + urlencode("https://beta.openplotter.cloud/settings");
|
||||
|
||||
-- get app_url
|
||||
SELECT
|
||||
value INTO app_url
|
||||
@@ -266,23 +275,28 @@ AS $pushover_subscribe_link$
|
||||
public.app_settings
|
||||
WHERE
|
||||
name = 'app.pushover_app_url';
|
||||
-- Generate OTP
|
||||
otp_code := api.generate_otp_fn(email);
|
||||
-- On sucess redirect to to API endpoing
|
||||
-- On success redirect to API endpoint
|
||||
SELECT CONCAT(
|
||||
'?success=',
|
||||
urlencode(CONCAT(app_url,'/api/rpc/pushover_fn?token=')),
|
||||
public.urlescape_py_fn(CONCAT(app_url,'/pushover?token=')),
|
||||
otp_code)
|
||||
INTO success;
|
||||
-- On failure redirect to user settings, where he does come from
|
||||
SELECT CONCAT(
|
||||
'&failure=',
|
||||
urlencode(CONCAT(app_url,'/settings'))
|
||||
public.urlescape_py_fn(CONCAT(app_url,'/profile'))
|
||||
) INTO failure;
|
||||
SELECT json_build_object( 'link', CONCAT(pushover_app_url, success, failure)) INTO pushover_link;
|
||||
SELECT json_build_object('link', CONCAT(pushover_app_url, success, failure)) INTO pushover_link;
|
||||
END;
|
||||
$pushover_subscribe_link$ language plpgsql security definer;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.pushover_subscribe_link_fn
|
||||
IS 'Generate Pushover subscription link';
|
||||
|
||||
-- Pushover Subscription API
|
||||
-- Confirm Pushover Subscription
|
||||
-- Web-Based Subscription Process
|
||||
-- https://pushover.net/api/subscriptions#web
|
||||
-- Expose as an API endpoint
|
||||
@@ -309,7 +323,7 @@ AS $pushover$
|
||||
DELETE FROM auth.otp
|
||||
WHERE user_email = _email;
|
||||
-- Disable Notification because
|
||||
-- Pushover send a notificataion when sucesssfull with the description of the app
|
||||
-- Pushover send a notification when sucesssful with the description of the app
|
||||
--
|
||||
-- Send Notification async
|
||||
--INSERT INTO process_queue (channel, payload, stored)
|
||||
@@ -322,7 +336,7 @@ $pushover$ language plpgsql security definer;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.pushover_fn
|
||||
IS 'Store pushover_user_key into user preferences if valid token/otp';
|
||||
IS 'Confirm Pushover Subscription and store pushover_user_key into user preferences if provide a valid OTP token';
|
||||
|
||||
-- Telegram OTP Validation
|
||||
-- Expose as an API endpoint
|
||||
@@ -331,7 +345,6 @@ CREATE OR REPLACE FUNCTION api.telegram_fn(IN token TEXT, IN telegram_obj TEXT)
|
||||
AS $telegram$
|
||||
DECLARE
|
||||
_email TEXT := NULL;
|
||||
_updated BOOLEAN := False;
|
||||
user_settings jsonb;
|
||||
BEGIN
|
||||
-- Check parameters
|
||||
@@ -344,14 +357,14 @@ AS $telegram$
|
||||
-- Set user email into env to allow RLS update
|
||||
PERFORM set_config('user.email', _email, false);
|
||||
-- Add telegram obj into user preferences
|
||||
SELECT api.update_user_preferences_fn('{telegram}'::TEXT, telegram_obj::TEXT) INTO _updated;
|
||||
PERFORM api.update_user_preferences_fn('{telegram}'::TEXT, telegram_obj::TEXT);
|
||||
-- Delete token when validated
|
||||
DELETE FROM auth.otp
|
||||
WHERE user_email = _email;
|
||||
-- Send Notification async
|
||||
INSERT INTO process_queue (channel, payload, stored)
|
||||
VALUES ('telegram_valid', _email, now());
|
||||
RETURN _updated;
|
||||
--INSERT INTO process_queue (channel, payload, stored)
|
||||
-- VALUES ('telegram_valid', _email, now());
|
||||
RETURN True;
|
||||
END IF;
|
||||
RETURN False;
|
||||
END;
|
||||
@@ -359,7 +372,7 @@ $telegram$ language plpgsql security definer;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.telegram_fn
|
||||
IS 'Store telegram chat details into user preferences if valid token/otp';
|
||||
IS 'Confirm telegram user and store telegram chat details into user preferences if provide a valid OTP token';
|
||||
|
||||
-- Telegram user validation
|
||||
DROP FUNCTION IF EXISTS auth.telegram_user_exists_fn;
|
||||
@@ -386,11 +399,11 @@ $telegram_user_exists$ language plpgsql security definer;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
auth.telegram_user_exists_fn
|
||||
IS 'Check if user exist based on email and telegram obj preferences';
|
||||
IS 'Check if user exist based on email and user_id';
|
||||
|
||||
-- Telegram otp validation
|
||||
DROP FUNCTION IF EXISTS auth.telegram_otp_fn;
|
||||
CREATE OR REPLACE FUNCTION auth.telegram_otp_fn(IN email TEXT, OUT otp_code TEXT) RETURNS TEXT
|
||||
DROP FUNCTION IF EXISTS api.telegram_otp_fn;
|
||||
CREATE OR REPLACE FUNCTION api.telegram_otp_fn(IN email TEXT, OUT otp_code TEXT) RETURNS TEXT
|
||||
AS $telegram_otp$
|
||||
DECLARE
|
||||
_email CITEXT := email;
|
||||
@@ -410,33 +423,40 @@ AS $telegram_otp$
|
||||
END IF;
|
||||
END;
|
||||
$telegram_otp$ language plpgsql security definer;
|
||||
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
auth.telegram_otp_fn
|
||||
IS 'TODO';
|
||||
api.telegram_otp_fn
|
||||
IS 'Telegram otp generation';
|
||||
|
||||
-- Telegram bot JWT auth
|
||||
-- Telegram JWT auth
|
||||
-- Expose as an API endpoint
|
||||
-- Avoid sending a password so use email and chat_id as key pair
|
||||
DROP FUNCTION IF EXISTS api.bot(text,BIGINT);
|
||||
CREATE OR REPLACE FUNCTION api.bot(IN email TEXT, IN user_id BIGINT) RETURNS auth.jwt_token
|
||||
AS $telegram_bot$
|
||||
DROP FUNCTION IF EXISTS api.telegram;
|
||||
CREATE OR REPLACE FUNCTION api.telegram(IN user_id BIGINT, IN email TEXT DEFAULT NULL) RETURNS auth.jwt_token
|
||||
AS $telegram_jwt$
|
||||
DECLARE
|
||||
_email TEXT := email;
|
||||
_user_id BIGINT := user_id;
|
||||
_uid TEXT := NULL;
|
||||
_exist BOOLEAN := False;
|
||||
result auth.jwt_token;
|
||||
app_jwt_secret text;
|
||||
BEGIN
|
||||
IF _email IS NULL OR _chat_id IS NULL THEN
|
||||
IF _user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
|
||||
END IF;
|
||||
-- check email and _chat_id
|
||||
select auth.telegram_user_exists_fn(_email, _user_id) into _exist;
|
||||
if _exist is null or _exist <> True then
|
||||
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
|
||||
end if;
|
||||
|
||||
-- Check _user_id
|
||||
SELECT auth.telegram_session_exists_fn(_user_id) into _exist;
|
||||
IF _exist IS NULL OR _exist <> True THEN
|
||||
--RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
-- Get email and user_id
|
||||
SELECT a.email,a.user_id INTO _email,_uid
|
||||
FROM auth.accounts a
|
||||
WHERE cast(preferences->'telegram'->'from'->'id' as BIGINT) = _user_id::BIGINT;
|
||||
|
||||
-- Get app_jwt_secret
|
||||
SELECT value INTO app_jwt_secret
|
||||
@@ -450,26 +470,28 @@ AS $telegram_bot$
|
||||
from (
|
||||
select 'user_role' as role,
|
||||
(select lower(_email)) as email,
|
||||
_uid as uid,
|
||||
extract(epoch from now())::integer + 60*60 as exp
|
||||
) r
|
||||
into result;
|
||||
return result;
|
||||
END;
|
||||
$telegram_bot$ language plpgsql security definer;
|
||||
$telegram_jwt$ language plpgsql security definer;
|
||||
-- Description
|
||||
COMMENT ON FUNCTION
|
||||
api.bot
|
||||
IS 'Generate a JWT user_role token from email for telegram bot';
|
||||
api.telegram
|
||||
IS 'Generate a JWT user_role token based on chat_id from telegram';
|
||||
|
||||
-- Telegram chat_id Session validation
|
||||
-- Telegram chat_id session validation
|
||||
DROP FUNCTION IF EXISTS auth.telegram_session_exists_fn;
|
||||
CREATE OR REPLACE FUNCTION auth.telegram_session_exists_fn(IN user_id BIGINT) RETURNS BOOLEAN
|
||||
AS $telegram_session_exists$
|
||||
DECLARE
|
||||
_id TEXT := NULL;
|
||||
_id BIGINT := NULL;
|
||||
_user_id BIGINT := user_id;
|
||||
_email TEXT := NULL;
|
||||
BEGIN
|
||||
IF _chat_id IS NULL THEN
|
||||
IF user_id IS NULL THEN
|
||||
RAISE EXCEPTION 'invalid input' USING HINT = 'check your parameter';
|
||||
END IF;
|
||||
|
||||
@@ -477,10 +499,10 @@ AS $telegram_session_exists$
|
||||
SELECT preferences->'telegram'->'from'->'id' INTO _id
|
||||
FROM auth.accounts a
|
||||
WHERE cast(preferences->'telegram'->'from'->'id' as BIGINT) = _user_id::BIGINT;
|
||||
IF NOT FOUND then
|
||||
RETURN False;
|
||||
END IF;
|
||||
IF FOUND THEN
|
||||
RETURN True;
|
||||
END IF;
|
||||
RETURN FALSE;
|
||||
END;
|
||||
$telegram_session_exists$ language plpgsql security definer;
|
||||
-- Description
|
||||
|
@@ -30,12 +30,14 @@ grant execute on function api.recover(text) to api_anonymous;
|
||||
grant execute on function api.reset(text,text,text) to api_anonymous;
|
||||
-- explicitly limit EXECUTE privileges to pgrest db-pre-request function
|
||||
grant execute on function public.check_jwt() to api_anonymous;
|
||||
-- explicitly limit EXECUTE privileges to only telegram bot auth function
|
||||
grant execute on function api.bot(text,bigint) to api_anonymous;
|
||||
-- explicitly limit EXECUTE privileges to only telegram jwt auth function
|
||||
grant execute on function api.telegram(bigint,text) to api_anonymous;
|
||||
-- explicitly limit EXECUTE privileges to only pushover subscription validation function
|
||||
grant execute on function api.email_fn(text) to api_anonymous;
|
||||
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;
|
||||
|
||||
-- authenticator
|
||||
-- login role
|
||||
@@ -44,24 +46,34 @@ 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 10 connections
|
||||
CREATE ROLE grafana WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 10 LOGIN PASSWORD 'mysecretpassword';
|
||||
-- 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';
|
||||
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;
|
||||
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 VIEWS
|
||||
-- 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.logs_view,api.moorages_view,api.stays_view,api.vessels_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;
|
||||
-- Allow Auth schema and Tables
|
||||
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 authticator user and role with login, read-only on auth.accounts, limit 10 connections
|
||||
CREATE ROLE grafana_auth WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 10 LOGIN PASSWORD 'mysecretpassword';
|
||||
-- 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';
|
||||
comment on role grafana_auth is
|
||||
'Role that grafana auth proxy authenticator via apache.';
|
||||
-- Allow read on VIEWS on API schema
|
||||
GRANT USAGE ON SCHEMA api TO grafana_auth;
|
||||
GRANT SELECT ON TABLE api.metadata TO grafana_auth;
|
||||
-- Allow Auth schema and Tables
|
||||
GRANT USAGE ON SCHEMA auth TO grafana_auth;
|
||||
--GRANT USAGE, SELECT ON SEQUENCE auth.accounts_pkey TO grafana_auth;
|
||||
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;
|
||||
|
||||
@@ -75,6 +87,7 @@ GRANT user_role to authenticator;
|
||||
GRANT USAGE ON SCHEMA api TO user_role;
|
||||
GRANT USAGE, SELECT ON SEQUENCE api.logbook_id_seq,api.metadata_id_seq,api.moorages_id_seq,api.stays_id_seq TO user_role;
|
||||
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
|
||||
@@ -91,31 +104,37 @@ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api 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_humidity,api.monitoring_voltage,api.monitoring_temperatures 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;
|
||||
--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;
|
||||
--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;
|
||||
--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;
|
||||
--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;
|
||||
ALTER VIEW api.vessel_p_view OWNER TO user_role;
|
||||
-- Remove all permissions except select and update
|
||||
REVOKE TRUNCATE, DELETE, TRIGGER, INSERT ON TABLE api.vessel_p_view FROM user_role;
|
||||
|
||||
|
||||
-- Vessel:
|
||||
@@ -145,7 +164,7 @@ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA _timescaledb_internal TO vessel_role;
|
||||
-- Crons
|
||||
--CREATE ROLE scheduler WITH NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION;
|
||||
CREATE ROLE scheduler WITH NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION CONNECTION LIMIT 10 LOGIN;
|
||||
comment on role vessel_role is
|
||||
comment on role scheduler is
|
||||
'Role that pgcron will use to process logbook,moorages,stays,monitoring and notification.';
|
||||
GRANT scheduler to authenticator;
|
||||
GRANT USAGE ON SCHEMA api TO scheduler;
|
||||
@@ -169,19 +188,23 @@ CREATE POLICY admin_all ON api.metadata TO current_user
|
||||
WITH CHECK (true);
|
||||
-- Allow vessel_role to insert and select on their own records
|
||||
CREATE POLICY api_vessel_role ON api.metadata TO vessel_role
|
||||
USING (client_id = current_setting('vessel.client_id', false))
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (true);
|
||||
-- Allow user_role to update and select on their own records
|
||||
CREATE POLICY api_user_role ON api.metadata TO user_role
|
||||
USING (client_id = current_setting('vessel.client_id', true))
|
||||
WITH CHECK (client_id = current_setting('vessel.client_id', false));
|
||||
-- Allow scheduler to update and select based on the client_id
|
||||
USING (vessel_id = current_setting('vessel.id', true))
|
||||
WITH CHECK (vessel_id = current_setting('vessel.id', false));
|
||||
-- Allow scheduler to update and select based on the vessel.id
|
||||
CREATE POLICY api_scheduler_role ON api.metadata TO scheduler
|
||||
USING (client_id = current_setting('vessel.client_id', false))
|
||||
WITH CHECK (client_id = current_setting('vessel.client_id', false));
|
||||
-- Allow grafana to select based on the client_id
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (vessel_id = current_setting('vessel.id', false));
|
||||
-- Allow grafana to select based on email
|
||||
CREATE POLICY grafana_role ON api.metadata TO grafana
|
||||
USING (client_id = client_id)
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
-- Allow grafana_auth to select
|
||||
CREATE POLICY grafana_proxy_role ON api.metadata TO grafana_auth
|
||||
USING (true)
|
||||
WITH CHECK (false);
|
||||
|
||||
ALTER TABLE api.metrics ENABLE ROW LEVEL SECURITY;
|
||||
@@ -191,19 +214,19 @@ CREATE POLICY admin_all ON api.metrics TO current_user
|
||||
WITH CHECK (true);
|
||||
-- Allow vessel_role to insert and select on their own records
|
||||
CREATE POLICY api_vessel_role ON api.metrics TO vessel_role
|
||||
USING (client_id = current_setting('vessel.client_id', false))
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (true);
|
||||
-- Allow user_role to update and select on their own records
|
||||
CREATE POLICY api_user_role ON api.metrics TO user_role
|
||||
USING (client_id = current_setting('vessel.client_id', true))
|
||||
WITH CHECK (client_id = current_setting('vessel.client_id', false));
|
||||
-- Allow scheduler to update and select based on the client_id
|
||||
USING (vessel_id = current_setting('vessel.id', true))
|
||||
WITH CHECK (vessel_id = current_setting('vessel.id', false));
|
||||
-- Allow scheduler to update and select based on the vessel.id
|
||||
CREATE POLICY api_scheduler_role ON api.metrics TO scheduler
|
||||
USING (client_id = current_setting('vessel.client_id', false))
|
||||
WITH CHECK (client_id = current_setting('vessel.client_id', false));
|
||||
-- Allow grafana to select based on the client_id
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (vessel_id = current_setting('vessel.id', false));
|
||||
-- Allow grafana to select based on the vessel.id
|
||||
CREATE POLICY grafana_role ON api.metrics TO grafana
|
||||
USING (client_id = client_id)
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
|
||||
-- Be sure to enable row level security on the table
|
||||
@@ -215,18 +238,19 @@ CREATE POLICY admin_all ON api.logbook TO current_user
|
||||
WITH CHECK (true);
|
||||
-- Allow vessel_role to insert and select on their own records
|
||||
CREATE POLICY api_vessel_role ON api.logbook TO vessel_role
|
||||
USING (client_id = current_setting('vessel.client_id', false))
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (true);
|
||||
-- Allow user_role to update and select on their own records
|
||||
CREATE POLICY api_user_role ON api.logbook TO user_role
|
||||
USING (client_id = current_setting('vessel.client_id', true))
|
||||
WITH CHECK (client_id = current_setting('vessel.client_id', false));
|
||||
-- Allow scheduler to update and select based on the client_id
|
||||
USING (vessel_id = current_setting('vessel.id', true))
|
||||
WITH CHECK (vessel_id = current_setting('vessel.id', false));
|
||||
-- Allow scheduler to update and select based on the vessel.id
|
||||
CREATE POLICY api_scheduler_role ON api.logbook TO scheduler
|
||||
USING (client_id = current_setting('vessel.client_id', false))
|
||||
WITH CHECK (client_id = current_setting('vessel.client_id', false));
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (vessel_id = current_setting('vessel.id', false));
|
||||
-- Allow grafana to select based on the vessel.id
|
||||
CREATE POLICY grafana_role ON api.logbook TO grafana
|
||||
USING (client_id = client_id)
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
|
||||
-- Be sure to enable row level security on the table
|
||||
@@ -237,19 +261,19 @@ CREATE POLICY admin_all ON api.stays TO current_user
|
||||
WITH CHECK (true);
|
||||
-- Allow vessel_role to insert and select on their own records
|
||||
CREATE POLICY api_vessel_role ON api.stays TO vessel_role
|
||||
USING (client_id = current_setting('vessel.client_id', false))
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (true);
|
||||
-- Allow user_role to update and select on their own records
|
||||
CREATE POLICY api_user_role ON api.stays TO user_role
|
||||
USING (client_id = current_setting('vessel.client_id', true))
|
||||
WITH CHECK (client_id = current_setting('vessel.client_id', false));
|
||||
-- Allow scheduler to update and select based on the client_id
|
||||
USING (vessel_id = current_setting('vessel.id', true))
|
||||
WITH CHECK (vessel_id = current_setting('vessel.id', false));
|
||||
-- Allow scheduler to update and select based on the vessel_id
|
||||
CREATE POLICY api_scheduler_role ON api.stays TO scheduler
|
||||
USING (client_id = current_setting('vessel.client_id', false))
|
||||
WITH CHECK (client_id = current_setting('vessel.client_id', false));
|
||||
-- Allow grafana to select based on the client_id
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (vessel_id = current_setting('vessel.id', false));
|
||||
-- Allow grafana to select based on the vessel_id
|
||||
CREATE POLICY grafana_role ON api.stays TO grafana
|
||||
USING (client_id = client_id)
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
|
||||
-- Be sure to enable row level security on the table
|
||||
@@ -260,19 +284,19 @@ CREATE POLICY admin_all ON api.moorages TO current_user
|
||||
WITH CHECK (true);
|
||||
-- Allow vessel_role to insert and select on their own records
|
||||
CREATE POLICY api_vessel_role ON api.moorages TO vessel_role
|
||||
USING (client_id = current_setting('vessel.client_id', false))
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (true);
|
||||
-- Allow user_role to update and select on their own records
|
||||
CREATE POLICY api_user_role ON api.moorages TO user_role
|
||||
USING (client_id = current_setting('vessel.client_id', true))
|
||||
WITH CHECK (client_id = current_setting('vessel.client_id', false));
|
||||
-- Allow scheduler to update and select based on the client_id
|
||||
USING (vessel_id = current_setting('vessel.id', true))
|
||||
WITH CHECK (vessel_id = current_setting('vessel.id', false));
|
||||
-- Allow scheduler to update and select based on the vessel_id
|
||||
CREATE POLICY api_scheduler_role ON api.moorages TO scheduler
|
||||
USING (client_id = current_setting('vessel.client_id', false))
|
||||
WITH CHECK (client_id = current_setting('vessel.client_id', false));
|
||||
-- Allow grafana to select based on the client_id
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (vessel_id = current_setting('vessel.id', false));
|
||||
-- Allow grafana to select based on the vessel_id
|
||||
CREATE POLICY grafana_role ON api.moorages TO grafana
|
||||
USING (client_id = client_id)
|
||||
USING (vessel_id = current_setting('vessel.id', false))
|
||||
WITH CHECK (false);
|
||||
|
||||
-- Be sure to enable row level security on the table
|
||||
@@ -289,6 +313,14 @@ CREATE POLICY api_user_role ON auth.vessels TO user_role
|
||||
WITH CHECK (vessel_id = current_setting('vessel.id', true)
|
||||
AND owner_email = current_setting('user.email', true)
|
||||
);
|
||||
-- Allow grafana to select based on email
|
||||
CREATE POLICY grafana_role ON auth.vessels TO grafana
|
||||
USING (owner_email = current_setting('user.email', true))
|
||||
WITH CHECK (false);
|
||||
-- Allow grafana to select
|
||||
CREATE POLICY grafana_proxy_role ON auth.vessels TO grafana_auth
|
||||
USING (true)
|
||||
WITH CHECK (false);
|
||||
|
||||
-- Be sure to enable row level security on the table
|
||||
ALTER TABLE auth.accounts ENABLE ROW LEVEL SECURITY;
|
||||
@@ -298,11 +330,32 @@ CREATE POLICY admin_all ON auth.accounts TO current_user
|
||||
WITH CHECK (true);
|
||||
-- Allow user_role to update and select on their own records
|
||||
CREATE POLICY api_user_role ON auth.accounts TO user_role
|
||||
USING (email = current_setting('user.email', true)
|
||||
)
|
||||
WITH CHECK (email = current_setting('user.email', true)
|
||||
);
|
||||
-- Allow grafana_auth to select based on the email
|
||||
USING (email = current_setting('user.email', true))
|
||||
WITH CHECK (email = current_setting('user.email', true));
|
||||
-- Allow scheduler see all rows and add any rows
|
||||
CREATE POLICY api_scheduler_role ON auth.accounts TO scheduler
|
||||
USING (email = current_setting('user.email', true))
|
||||
WITH CHECK (email = current_setting('user.email', true));
|
||||
-- Allow grafana_auth to select
|
||||
CREATE POLICY grafana_proxy_role ON auth.accounts TO grafana_auth
|
||||
USING (email = email)
|
||||
USING (true)
|
||||
WITH CHECK (false);
|
||||
|
||||
-- Be sure to enable row level security on the table
|
||||
ALTER TABLE public.process_queue ENABLE ROW LEVEL SECURITY;
|
||||
-- Administrator can see all rows and add any rows
|
||||
CREATE POLICY admin_all ON public.process_queue TO current_user
|
||||
USING (true)
|
||||
WITH CHECK (true);
|
||||
-- Allow vessel_role to insert and select on their own records
|
||||
CREATE POLICY api_vessel_role ON public.process_queue TO vessel_role
|
||||
USING (ref_id = current_setting('user.id', true) OR ref_id = current_setting('vessel.id', true))
|
||||
WITH CHECK (true);
|
||||
-- Allow user_role to update and select on their own records
|
||||
CREATE POLICY api_user_role ON public.process_queue TO user_role
|
||||
USING (ref_id = current_setting('user.id', true) OR ref_id = current_setting('vessel.id', true))
|
||||
WITH CHECK (ref_id = current_setting('user.id', true) OR ref_id = current_setting('vessel.id', true));
|
||||
-- Allow scheduler see all rows and updates any rows
|
||||
CREATE POLICY api_scheduler_role ON public.process_queue TO scheduler
|
||||
USING (true)
|
||||
WITH CHECK (false);
|
||||
|
@@ -15,11 +15,11 @@ SELECT cron.schedule('cron_new_logbook', '*/5 * * * *', 'select public.cron_proc
|
||||
--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', '*/5 * * * *', 'select public.cron_process_new_stay_fn()');
|
||||
SELECT cron.schedule('cron_new_stay', '*/6 * * * *', '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', '*/6 * * * *', '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
|
||||
@@ -44,18 +44,26 @@ SELECT cron.schedule('cron_monitor_online', '*/10 * * * *', 'select public.cron_
|
||||
|
||||
-- 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', '*/5 * * * *', 'select public.cron_process_new_notification_fn()');
|
||||
SELECT cron.schedule('cron_new_notification', '*/2 * * * *', '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.”
|
||||
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;');
|
||||
-- Any other maintenance require?
|
||||
|
||||
-- OTP
|
||||
-- Create a every 15 minute job cron_process_prune_otp_fn
|
||||
SELECT cron.schedule('cron_prune_otp', '*/15 * * * *', 'select public.cron_process_prune_otp_fn()');
|
||||
|
||||
-- Alerts
|
||||
-- Create a every 11 minute job cron_process_alerts_fn
|
||||
--SELECT cron.schedule('cron_alerts', '*/11 * * * *', 'select public.cron_process_alerts_fn()');
|
||||
|
||||
-- Cron job settings
|
||||
UPDATE cron.job SET database = 'signalk';
|
||||
UPDATE cron.job SET username = 'username'; -- TODO update to scheduler, pending process_queue update
|
||||
@@ -70,6 +78,6 @@ SELECT * FROM cron.job;
|
||||
-- TRUNCATE TABLE cron.job_run_details
|
||||
--TRUNCATE TABLE cron.job_run_details CONTINUE IDENTITY RESTRICT;
|
||||
-- check job log
|
||||
select * from cron.job_run_details ORDER BY end_time DESC LIMIT 10;
|
||||
SELECT * FROM cron.job_run_details ORDER BY end_time DESC;
|
||||
-- DEBUG Disable all
|
||||
UPDATE cron.job SET active = False;
|
76
initdb/07naturalearthdata.sql
Normal file
@@ -0,0 +1,76 @@
|
||||
---------------------------------------------------------------------------
|
||||
-- https://www.naturalearthdata.com
|
||||
--
|
||||
-- https://naciscdn.org/naturalearth/10m/physical/ne_10m_geography_marine_polys.zip
|
||||
--
|
||||
-- https://github.com/nvkelso/natural-earth-vector/raw/master/10m_physical/ne_10m_geography_marine_polys.shp
|
||||
--
|
||||
|
||||
-- Import from shapefile
|
||||
-- # shp2pgsql ne_10m_geography_marine_polys.shp public.ne_10m_geography_marine_polys | psql -U ${POSTGRES_USER} signalk
|
||||
--
|
||||
-- PostgSail Customization, add tropics and alaska area.
|
||||
|
||||
-- List current database
|
||||
select current_database();
|
||||
|
||||
-- connect to the DB
|
||||
\c signalk
|
||||
|
||||
CREATE TABLE public.ne_10m_geography_marine_polys (
|
||||
gid serial4 NOT NULL,
|
||||
featurecla TEXT NULL,
|
||||
"name" TEXT NULL,
|
||||
namealt TEXT NULL,
|
||||
changed TEXT NULL,
|
||||
note TEXT NULL,
|
||||
name_fr TEXT NULL,
|
||||
min_label float8 NULL,
|
||||
max_label float8 NULL,
|
||||
scalerank int2 NULL,
|
||||
"label" TEXT NULL,
|
||||
wikidataid TEXT NULL,
|
||||
name_ar TEXT NULL,
|
||||
name_bn TEXT NULL,
|
||||
name_de TEXT NULL,
|
||||
name_en TEXT NULL,
|
||||
name_es TEXT NULL,
|
||||
name_el TEXT NULL,
|
||||
name_hi TEXT NULL,
|
||||
name_hu TEXT NULL,
|
||||
name_id TEXT NULL,
|
||||
name_it TEXT NULL,
|
||||
name_ja TEXT NULL,
|
||||
name_ko TEXT NULL,
|
||||
name_nl TEXT NULL,
|
||||
name_pl TEXT NULL,
|
||||
name_pt TEXT NULL,
|
||||
name_ru TEXT NULL,
|
||||
name_sv TEXT NULL,
|
||||
name_tr TEXT NULL,
|
||||
name_vi TEXT NULL,
|
||||
name_zh TEXT NULL,
|
||||
ne_id int8 NULL,
|
||||
name_fa TEXT NULL,
|
||||
name_he TEXT NULL,
|
||||
name_uk TEXT NULL,
|
||||
name_ur TEXT NULL,
|
||||
name_zht TEXT NULL,
|
||||
geom geometry(multipolygon,4326) NULL,
|
||||
CONSTRAINT ne_10m_geography_marine_polys_pkey PRIMARY KEY (gid)
|
||||
);
|
||||
-- Add GIST index
|
||||
CREATE INDEX ne_10m_geography_marine_polys_geom_idx
|
||||
ON public.ne_10m_geography_marine_polys
|
||||
USING GIST (geom);
|
||||
|
||||
-- Description
|
||||
COMMENT ON TABLE
|
||||
public.ne_10m_geography_marine_polys
|
||||
IS 'imperfect but light weight geographic marine areas from https://www.naturalearthdata.com';
|
||||
|
||||
-- Import data
|
||||
COPY public.ne_10m_geography_marine_polys(gid,featurecla,"name",namealt,changed,note,name_fr,min_label,max_label,scalerank,"label",wikidataid,name_ar,name_bn,name_de,name_en,name_es,name_el,name_hi,name_hu,name_id,name_it,name_ja,name_ko,name_nl,name_pl,name_pt,name_ru,name_sv,name_tr,name_vi,name_zh,ne_id,name_fa,name_he,name_uk,name_ur,name_zht,geom)
|
||||
FROM '/docker-entrypoint-initdb.d/ne_10m_geography_marine_polys.csv'
|
||||
DELIMITER ','
|
||||
CSV HEADER;
|
@@ -1 +1 @@
|
||||
0.0.9
|
||||
0.2.2
|
||||
|
310
initdb/ne_10m_geography_marine_polys.csv
Normal file
13
pgadmin_servers.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"Servers": {
|
||||
"dev": {
|
||||
"Name": "PostgSail dev db",
|
||||
"Group": "Servers",
|
||||
"Port": 5432,
|
||||
"Host": "db",
|
||||
"SSLMode": "prefer",
|
||||
"MaintenanceDB": "postgres",
|
||||
"Username": "postgres"
|
||||
}
|
||||
}
|
||||
}
|
8
tests/Dockerfile
Normal file
@@ -0,0 +1,8 @@
|
||||
FROM node:lts
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
# Install and update the system
|
||||
RUN apt-get -q update && apt-get -qy upgrade && apt-get -qy install postgresql-client
|
||||
# Clean up APT when done.
|
||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
18
tests/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# PostgSail Unit Tests
|
||||
The Unit Tests allow to automatically validate api workflow.
|
||||
|
||||
## A global overview
|
||||
Based on `mocha` & `psql`
|
||||
|
||||
## get started
|
||||
```bash
|
||||
$ npm i
|
||||
$ alias mocha="./node_modules/.bin/mocha"
|
||||
$ bash tests.sh
|
||||
```
|
||||
|
||||
## docker
|
||||
```bash
|
||||
$ docker-compose up -d db && sleep 15 && docker-compose up -d api && sleep 5
|
||||
$ docker-compose -f docker-compose.dev.yml -f docker-compose.yml up tests
|
||||
```
|