Compare commits
73 Commits
haaavk/mas
...
v0.10.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e43718f596 | ||
|
|
d9623a9905 | ||
|
|
011f1f13c8 | ||
|
|
9832de0c19 | ||
|
|
83b20643d2 | ||
|
|
ab44ba8318 | ||
|
|
fcea6d3be9 | ||
|
|
f3a89bff78 | ||
|
|
3c9bc9f3c9 | ||
|
|
2f5d0ba7e5 | ||
|
|
1c866209c9 | ||
|
|
a4785b1a0c | ||
|
|
2928e663db | ||
|
|
ff97a46fd9 | ||
|
|
afb78dc499 | ||
|
|
a4eaef0117 | ||
|
|
7891866214 | ||
|
|
eedcbc4e85 | ||
|
|
0d006620dd | ||
|
|
0b78f6df72 | ||
|
|
74ddef1670 | ||
|
|
9d9d4a7b1e | ||
|
|
d66f683104 | ||
|
|
b44642e023 | ||
|
|
c12a7e9e71 | ||
|
|
0294d31ea4 | ||
|
|
40cb5afbad | ||
|
|
073bd94112 | ||
|
|
3a01fefcff | ||
|
|
14a7ec68f3 | ||
|
|
fdf2ab719b | ||
|
|
737eeb5df4 | ||
|
|
cb4855e4fc | ||
|
|
f4127cf9b1 | ||
|
|
159015de1c | ||
|
|
a7548d7eba | ||
|
|
da87ddb18f | ||
|
|
4a76ab32fc | ||
|
|
4afeced7d3 | ||
|
|
2a6efe1b7f | ||
|
|
07f3926a9c | ||
|
|
14ed0b7979 | ||
|
|
ab51089647 | ||
|
|
86695dbcc4 | ||
|
|
4e4cfe081b | ||
|
|
f54b67ef0f | ||
|
|
43f339e32b | ||
|
|
b144efaa9b | ||
|
|
c06b7a094a | ||
|
|
f13745f15e | ||
|
|
a1a083a403 | ||
|
|
8b167b2c74 | ||
|
|
4cd0c4735d | ||
|
|
d9e1ffddb1 | ||
|
|
9fb875f749 | ||
|
|
f6e502dfbd | ||
|
|
7c69b0bd81 | ||
|
|
78bea501a8 | ||
|
|
c2daf3a5a5 | ||
|
|
df6786e037 | ||
|
|
6621625d90 | ||
|
|
32ae0aa5f3 | ||
|
|
2221a99662 | ||
|
|
69ec37331a | ||
|
|
ea893b2322 | ||
|
|
e9536f1816 | ||
|
|
6f835a4f27 | ||
|
|
faf4f48e75 | ||
|
|
278306daa4 | ||
|
|
2c0fafefea | ||
|
|
6eb41e016a | ||
|
|
369f4d8d6b | ||
|
|
3d43f223eb |
43
.github/workflows/build-docker-edge.yml
vendored
Normal file
43
.github/workflows/build-docker-edge.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Build edge Docker images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish_to_docker_hub:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Prepare tags
|
||||||
|
id: prep
|
||||||
|
run: |
|
||||||
|
DOCKER_IMAGE=milesmcc/shynet
|
||||||
|
TAGS="${DOCKER_IMAGE}:edge"
|
||||||
|
echo ::set-output name=tags::${TAGS}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push advanced image
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.prep.outputs.tags }}
|
||||||
44
.github/workflows/build-docker-release.yml
vendored
Normal file
44
.github/workflows/build-docker-release.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Build release Docker images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish_to_docker_hub:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Prepare tags
|
||||||
|
id: prep
|
||||||
|
run: |
|
||||||
|
DOCKER_IMAGE=milesmcc/shynet
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:latest"
|
||||||
|
echo ::set-output name=tags::${TAGS}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push advanced image
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.prep.outputs.tags }}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3-alpine
|
FROM python:alpine3.12
|
||||||
|
|
||||||
# Getting things ready
|
# Getting things ready
|
||||||
WORKDIR /usr/src/shynet
|
WORKDIR /usr/src/shynet
|
||||||
|
|||||||
39
Pipfile.lock
generated
39
Pipfile.lock
generated
@@ -19,6 +19,7 @@
|
|||||||
"sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21",
|
"sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21",
|
||||||
"sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"
|
"sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
"version": "==2.6.1"
|
"version": "==2.6.1"
|
||||||
},
|
},
|
||||||
"asgiref": {
|
"asgiref": {
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
|
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
|
||||||
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
|
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==3.3.4"
|
"version": "==3.3.4"
|
||||||
},
|
},
|
||||||
"billiard": {
|
"billiard": {
|
||||||
@@ -45,16 +47,17 @@
|
|||||||
},
|
},
|
||||||
"certifi": {
|
"certifi": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
|
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
|
||||||
"sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
|
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
|
||||||
],
|
],
|
||||||
"version": "==2020.12.5"
|
"version": "==2021.5.30"
|
||||||
},
|
},
|
||||||
"chardet": {
|
"chardet": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
"version": "==4.0.0"
|
"version": "==4.0.0"
|
||||||
},
|
},
|
||||||
"defusedxml": {
|
"defusedxml": {
|
||||||
@@ -62,15 +65,16 @@
|
|||||||
"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
|
"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
|
||||||
"sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"
|
"sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
"version": "==0.7.1"
|
"version": "==0.7.1"
|
||||||
},
|
},
|
||||||
"django": {
|
"django": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:c348b3ddc452bf4b62361f0752f71a339140c777ebea3cdaaaa8fdb7f417a862",
|
"sha256:a523d62b7ab2908f551dabc32b99017a86aa7784e32b761708e52be3dce6d35d",
|
||||||
"sha256:f8393103e15ec2d2d313ccbb95a3f1da092f9f58d74ac1c61ca2ac0436ae1eac"
|
"sha256:dc41bf07357f1f4810c1c555b685cb51f780b41e37892d6cc92b89789f2847e1"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==3.1.8"
|
"version": "==3.1.12"
|
||||||
},
|
},
|
||||||
"django-allauth": {
|
"django-allauth": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -145,6 +149,7 @@
|
|||||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==2.10"
|
"version": "==2.10"
|
||||||
},
|
},
|
||||||
"kombu": {
|
"kombu": {
|
||||||
@@ -152,20 +157,23 @@
|
|||||||
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
|
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
|
||||||
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
|
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
"version": "==4.6.11"
|
"version": "==4.6.11"
|
||||||
},
|
},
|
||||||
"maxminddb": {
|
"maxminddb": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
|
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==2.0.3"
|
"version": "==2.0.3"
|
||||||
},
|
},
|
||||||
"oauthlib": {
|
"oauthlib": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
"sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc",
|
||||||
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
|
"sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"
|
||||||
],
|
],
|
||||||
"version": "==3.1.0"
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==3.1.1"
|
||||||
},
|
},
|
||||||
"psycopg2-binary": {
|
"psycopg2-binary": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -277,12 +285,14 @@
|
|||||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||||
"version": "==2.25.1"
|
"version": "==2.25.1"
|
||||||
},
|
},
|
||||||
"requests-oauthlib": {
|
"requests-oauthlib": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
||||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
|
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
|
||||||
|
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
|
||||||
],
|
],
|
||||||
"version": "==1.3.0"
|
"version": "==1.3.0"
|
||||||
},
|
},
|
||||||
@@ -298,6 +308,7 @@
|
|||||||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '3.5'",
|
||||||
"version": "==0.4.1"
|
"version": "==0.4.1"
|
||||||
},
|
},
|
||||||
"ua-parser": {
|
"ua-parser": {
|
||||||
@@ -310,10 +321,11 @@
|
|||||||
},
|
},
|
||||||
"urllib3": {
|
"urllib3": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",
|
"sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
|
||||||
"sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"
|
"sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
|
||||||
],
|
],
|
||||||
"version": "==1.26.4"
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
|
||||||
|
"version": "==1.26.5"
|
||||||
},
|
},
|
||||||
"user-agents": {
|
"user-agents": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -328,6 +340,7 @@
|
|||||||
"sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
|
"sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
|
||||||
"sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
|
"sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
|
||||||
],
|
],
|
||||||
|
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||||
"version": "==1.3.0"
|
"version": "==1.3.0"
|
||||||
},
|
},
|
||||||
"whitenoise": {
|
"whitenoise": {
|
||||||
|
|||||||
18
TEMPLATE.env
18
TEMPLATE.env
@@ -33,6 +33,8 @@ ACCOUNT_SIGNUPS_ENABLED=False
|
|||||||
ACCOUNT_EMAIL_VERIFICATION=none
|
ACCOUNT_EMAIL_VERIFICATION=none
|
||||||
|
|
||||||
# The timezone of the admin panel. Affects how dates are displayed.
|
# The timezone of the admin panel. Affects how dates are displayed.
|
||||||
|
# This must match a value from the IANA's tz database.
|
||||||
|
# Wikipedia has a list of valid strings: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
|
||||||
TIME_ZONE=America/New_York
|
TIME_ZONE=America/New_York
|
||||||
|
|
||||||
# Set to "False" if you will not be serving content over HTTPS
|
# Set to "False" if you will not be serving content over HTTPS
|
||||||
@@ -83,3 +85,19 @@ BLOCK_ALL_IPS=False
|
|||||||
# keys) aren't supplied. It will also prevent sessions from spanning
|
# keys) aren't supplied. It will also prevent sessions from spanning
|
||||||
# one day to another.
|
# one day to another.
|
||||||
AGGRESSIVE_HASH_SALTING=True
|
AGGRESSIVE_HASH_SALTING=True
|
||||||
|
|
||||||
|
# Custom location url to link to in frontend.
|
||||||
|
# $LATITUDE will get replaced by the latitude, $LONGITUDE will get
|
||||||
|
# replaced by the longitude.
|
||||||
|
# Examples:
|
||||||
|
# - https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE (default)
|
||||||
|
# - https://www.google.com/maps/search/?api=1&query=$LATITUDE,$LONGITUDE
|
||||||
|
# - https://www.mapquest.com/near-$LATITUDE,$LONGITUDE
|
||||||
|
LOCATION_URL=https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE
|
||||||
|
|
||||||
|
# How many services should be displayed on dashboard page?
|
||||||
|
# Set to big number if you don't want pagination at all.
|
||||||
|
DASHBOARD_PAGE_SIZE=5
|
||||||
|
|
||||||
|
# Should background bars be scaled to full width?
|
||||||
|
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION=True
|
||||||
|
|||||||
15
app.json
15
app.json
@@ -122,6 +122,21 @@
|
|||||||
"description": "Set to 'False' if you do not want the version to be displayed on the frontend.",
|
"description": "Set to 'False' if you do not want the version to be displayed on the frontend.",
|
||||||
"value": "True",
|
"value": "True",
|
||||||
"required": false
|
"required": false
|
||||||
|
},
|
||||||
|
"LOCATION_URL": {
|
||||||
|
"description": "Custom location url to link to in frontend.",
|
||||||
|
"value": "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"DASHBOARD_PAGE_SIZE": {
|
||||||
|
"description": "How many services should be displayed on dashboard page?",
|
||||||
|
"value": "5",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"USE_RELATIVE_MAX_IN_BAR_VISUALIZATION": {
|
||||||
|
"description": "Should background bars be scaled to full width?",
|
||||||
|
"value": "True",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: "shynet-webserver"
|
- name: "shynet-webserver"
|
||||||
image: "milesmcc/shynet:dev"
|
image: "milesmcc/shynet:latest"
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
envFrom:
|
envFrom:
|
||||||
- secretRef:
|
- secretRef:
|
||||||
@@ -42,7 +42,7 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: "shynet-celeryworker"
|
- name: "shynet-celeryworker"
|
||||||
image: "milesmcc/shynet:dev"
|
image: "milesmcc/shynet:latest"
|
||||||
command: ["./celeryworker.sh"]
|
command: ["./celeryworker.sh"]
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
envFrom:
|
envFrom:
|
||||||
|
|||||||
1179
package-lock.json
generated
1179
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,8 +20,9 @@
|
|||||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||||
"a17t": "^0.5.1",
|
"a17t": "^0.5.1",
|
||||||
"apexcharts": "^3.24.0",
|
"apexcharts": "^3.24.0",
|
||||||
|
"datamaps": "^0.5.9",
|
||||||
"flag-icon-css": "^3.5.0",
|
"flag-icon-css": "^3.5.0",
|
||||||
"inter-ui": "^3.15.0",
|
"inter-ui": "^3.15.0",
|
||||||
"litepicker": "^1.5.7"
|
"litepicker": "^2.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,14 @@
|
|||||||
var Shynet = {
|
var Shynet = {
|
||||||
idempotency: null,
|
idempotency: null,
|
||||||
heartbeatTaskId: null,
|
heartbeatTaskId: null,
|
||||||
|
skipHeartbeat: false,
|
||||||
sendHeartbeat: function () {
|
sendHeartbeat: function () {
|
||||||
try {
|
try {
|
||||||
if (document.hidden) {
|
if (document.hidden || Shynet.skipHeartbeat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Shynet.skipHeartbeat = true;
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open(
|
xhr.open(
|
||||||
"POST",
|
"POST",
|
||||||
@@ -20,6 +23,12 @@ var Shynet = {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
xhr.setRequestHeader("Content-Type", "application/json");
|
xhr.setRequestHeader("Content-Type", "application/json");
|
||||||
|
xhr.onload = function () {
|
||||||
|
Shynet.skipHeartbeat = false;
|
||||||
|
};
|
||||||
|
xhr.onerror = function () {
|
||||||
|
Shynet.skipHeartbeat = false;
|
||||||
|
};
|
||||||
xhr.send(
|
xhr.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
idempotency: Shynet.idempotency,
|
idempotency: Shynet.idempotency,
|
||||||
@@ -37,6 +46,7 @@ var Shynet = {
|
|||||||
clearInterval(Shynet.heartbeatTaskId);
|
clearInterval(Shynet.heartbeatTaskId);
|
||||||
}
|
}
|
||||||
Shynet.idempotency = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
Shynet.idempotency = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||||
|
Shynet.skipHeartbeat = false;
|
||||||
Shynet.heartbeatTaskId = setInterval(Shynet.sendHeartbeat, parseInt("{{heartbeat_frequency}}"));
|
Shynet.heartbeatTaskId = setInterval(Shynet.sendHeartbeat, parseInt("{{heartbeat_frequency}}"));
|
||||||
Shynet.sendHeartbeat();
|
Shynet.sendHeartbeat();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.functions import TruncDate
|
from django.db.models.functions import TruncDate, TruncHour
|
||||||
from django.db.utils import NotSupportedError
|
from django.db.utils import NotSupportedError
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# How long a session a needs to go without an update to no longer be considered 'active' (i.e., currently online)
|
||||||
ACTIVE_USER_TIMEDELTA = timezone.timedelta(
|
ACTIVE_USER_TIMEDELTA = timezone.timedelta(
|
||||||
milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2
|
milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2
|
||||||
)
|
)
|
||||||
@@ -125,8 +125,10 @@ class Service(models.Model):
|
|||||||
Session = apps.get_model("analytics", "Session")
|
Session = apps.get_model("analytics", "Session")
|
||||||
Hit = apps.get_model("analytics", "Hit")
|
Hit = apps.get_model("analytics", "Hit")
|
||||||
|
|
||||||
|
tz_now = timezone.now()
|
||||||
|
|
||||||
currently_online = Session.objects.filter(
|
currently_online = Session.objects.filter(
|
||||||
service=self, last_seen__gt=timezone.now() - ACTIVE_USER_TIMEDELTA
|
service=self, last_seen__gt=tz_now - ACTIVE_USER_TIMEDELTA
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
sessions = Session.objects.filter(
|
sessions = Session.objects.filter(
|
||||||
@@ -196,6 +198,36 @@ class Service(models.Model):
|
|||||||
|
|
||||||
avg_hits_per_session = hit_count / session_count if session_count > 0 else None
|
avg_hits_per_session = hit_count / session_count if session_count > 0 else None
|
||||||
|
|
||||||
|
avg_session_duration = self._get_avg_session_duration(sessions, session_count)
|
||||||
|
|
||||||
|
chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data(
|
||||||
|
sessions, hits, start_time, end_time, tz_now
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"currently_online": currently_online,
|
||||||
|
"session_count": session_count,
|
||||||
|
"hit_count": hit_count,
|
||||||
|
"has_hits": has_hits,
|
||||||
|
"bounce_rate_pct": bounce_count * 100 / session_count
|
||||||
|
if session_count > 0
|
||||||
|
else None,
|
||||||
|
"avg_session_duration": avg_session_duration,
|
||||||
|
"avg_load_time": avg_load_time,
|
||||||
|
"avg_hits_per_session": avg_hits_per_session,
|
||||||
|
"locations": locations,
|
||||||
|
"referrers": referrers,
|
||||||
|
"countries": countries,
|
||||||
|
"operating_systems": operating_systems,
|
||||||
|
"browsers": browsers,
|
||||||
|
"devices": devices,
|
||||||
|
"device_types": device_types,
|
||||||
|
"chart_data": chart_data,
|
||||||
|
"chart_tooltip_format": chart_tooltip_format,
|
||||||
|
"chart_granularity": chart_granularity,
|
||||||
|
"online": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_avg_session_duration(self, sessions, session_count):
|
||||||
try:
|
try:
|
||||||
avg_session_duration = sessions.annotate(
|
avg_session_duration = sessions.annotate(
|
||||||
duration=models.F("last_seen") - models.F("start_time")
|
duration=models.F("last_seen") - models.F("start_time")
|
||||||
@@ -210,48 +242,72 @@ class Service(models.Model):
|
|||||||
if session_count == 0:
|
if session_count == 0:
|
||||||
avg_session_duration = None
|
avg_session_duration = None
|
||||||
|
|
||||||
session_chart_data = {
|
return avg_session_duration
|
||||||
k["date"]: k["count"]
|
|
||||||
for k in sessions.annotate(date=TruncDate("start_time"))
|
def _get_chart_data(self, sessions, hits, start_time, end_time, tz_now):
|
||||||
|
# Show hourly chart for date ranges of 3 days or less, otherwise daily chart
|
||||||
|
if (end_time - start_time).days < 3:
|
||||||
|
chart_tooltip_format = "MM/dd HH:mm"
|
||||||
|
chart_granularity = "hourly"
|
||||||
|
sessions_per_hour = (
|
||||||
|
sessions.annotate(hour=TruncHour("start_time"))
|
||||||
|
.values("hour")
|
||||||
|
.annotate(count=models.Count("uuid"))
|
||||||
|
.order_by("hour")
|
||||||
|
)
|
||||||
|
chart_data = {
|
||||||
|
k["hour"]: {"sessions": k["count"]} for k in sessions_per_hour
|
||||||
|
}
|
||||||
|
hits_per_hour = (
|
||||||
|
hits.annotate(hour=TruncHour("start_time"))
|
||||||
|
.values("hour")
|
||||||
|
.annotate(count=models.Count("id"))
|
||||||
|
.order_by("hour")
|
||||||
|
)
|
||||||
|
for k in hits_per_hour:
|
||||||
|
if k["hour"] not in chart_data:
|
||||||
|
chart_data[k["hour"]] = {"hits": k["count"], "sessions": 0}
|
||||||
|
else:
|
||||||
|
chart_data[k["hour"]]["hits"] = k["count"]
|
||||||
|
|
||||||
|
hours_range = range(int((end_time - start_time).total_seconds() / 3600) + 1)
|
||||||
|
for hour_offset in hours_range:
|
||||||
|
hour = start_time + timezone.timedelta(hours=hour_offset)
|
||||||
|
if hour not in chart_data and hour <= tz_now:
|
||||||
|
chart_data[hour] = {"sessions": 0, "hits": 0}
|
||||||
|
else:
|
||||||
|
chart_tooltip_format = "MMM d"
|
||||||
|
chart_granularity = "daily"
|
||||||
|
sessions_per_day = (
|
||||||
|
sessions.annotate(date=TruncDate("start_time"))
|
||||||
.values("date")
|
.values("date")
|
||||||
.annotate(count=models.Count("uuid"))
|
.annotate(count=models.Count("uuid"))
|
||||||
.order_by("date")
|
.order_by("date")
|
||||||
}
|
)
|
||||||
|
chart_data = {k["date"]: {"sessions": k["count"]} for k in sessions_per_day}
|
||||||
|
hits_per_day = (
|
||||||
|
hits.annotate(date=TruncDate("start_time"))
|
||||||
|
.values("date")
|
||||||
|
.annotate(count=models.Count("id"))
|
||||||
|
.order_by("date")
|
||||||
|
)
|
||||||
|
for k in hits_per_day:
|
||||||
|
chart_data[k["date"]]["hits"] = k["count"]
|
||||||
|
|
||||||
for day_offset in range((end_time - start_time).days + 1):
|
for day_offset in range((end_time - start_time).days + 1):
|
||||||
day = (start_time + timezone.timedelta(days=day_offset)).date()
|
day = (start_time + timezone.timedelta(days=day_offset)).date()
|
||||||
if day not in session_chart_data:
|
if day not in chart_data and day <= tz_now.date():
|
||||||
session_chart_data[day] = 0
|
chart_data[day] = {"sessions": 0, "hits": 0}
|
||||||
|
|
||||||
return {
|
chart_data = sorted(chart_data.items(), key=lambda k: k[0])
|
||||||
"currently_online": currently_online,
|
chart_data = {
|
||||||
"session_count": session_count,
|
'sessions': [v['sessions'] for k, v in chart_data],
|
||||||
"hit_count": hit_count,
|
'hits': [v['hits'] for k, v in chart_data],
|
||||||
"has_hits": has_hits,
|
'labels': [str(k) for k, v in chart_data],
|
||||||
"avg_hits_per_session": hit_count / (max(session_count, 1)),
|
|
||||||
"bounce_rate_pct": bounce_count * 100 / session_count
|
|
||||||
if session_count > 0
|
|
||||||
else None,
|
|
||||||
"avg_session_duration": avg_session_duration,
|
|
||||||
"avg_load_time": avg_load_time,
|
|
||||||
"avg_hits_per_session": avg_hits_per_session,
|
|
||||||
"locations": locations,
|
|
||||||
"referrers": referrers,
|
|
||||||
"countries": countries,
|
|
||||||
"operating_systems": operating_systems,
|
|
||||||
"browsers": browsers,
|
|
||||||
"devices": devices,
|
|
||||||
"device_types": device_types,
|
|
||||||
"session_chart_data": json.dumps(
|
|
||||||
[
|
|
||||||
{"x": str(key), "y": value}
|
|
||||||
for key, value in sorted(
|
|
||||||
session_chart_data.items(), key=lambda k: k[0]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
"online": True,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return chart_data, chart_tooltip_format, chart_granularity
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse(
|
return reverse(
|
||||||
"dashboard:service",
|
"dashboard:service",
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from datetime import datetime, time
|
from datetime import datetime, time
|
||||||
from urllib.parse import urlparse
|
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
class DateRangeMixin:
|
class DateRangeMixin:
|
||||||
def get_start_date(self):
|
def get_start_date(self):
|
||||||
if self.request.GET.get("startDate") != None:
|
if self.request.GET.get("startDate") is not None:
|
||||||
found_time = timezone.datetime.strptime(
|
found_time = timezone.datetime.strptime(
|
||||||
self.request.GET.get("startDate"), "%Y-%m-%d"
|
self.request.GET.get("startDate"), "%Y-%m-%d"
|
||||||
)
|
)
|
||||||
@@ -15,7 +14,7 @@ class DateRangeMixin:
|
|||||||
return timezone.now() - timezone.timedelta(days=30)
|
return timezone.now() - timezone.timedelta(days=30)
|
||||||
|
|
||||||
def get_end_date(self):
|
def get_end_date(self):
|
||||||
if self.request.GET.get("endDate") != None:
|
if self.request.GET.get("endDate") is not None:
|
||||||
found_time = timezone.datetime.strptime(
|
found_time = timezone.datetime.strptime(
|
||||||
self.request.GET.get("endDate"), "%Y-%m-%d"
|
self.request.GET.get("endDate"), "%Y-%m-%d"
|
||||||
)
|
)
|
||||||
@@ -23,8 +22,45 @@ class DateRangeMixin:
|
|||||||
else:
|
else:
|
||||||
return timezone.now()
|
return timezone.now()
|
||||||
|
|
||||||
|
def get_date_ranges(self):
|
||||||
|
now = timezone.now()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'name': 'Last 3 days',
|
||||||
|
'start': now - timezone.timedelta(days=2),
|
||||||
|
'end': now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Last 30 days',
|
||||||
|
'start': now - timezone.timedelta(days=29),
|
||||||
|
'end': now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Last 90 days',
|
||||||
|
'start': now - timezone.timedelta(days=89),
|
||||||
|
'end': now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'This month',
|
||||||
|
'start': now.replace(day=1),
|
||||||
|
'end': now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'Last month',
|
||||||
|
'start': now.replace(day=1, month=now.month - 1),
|
||||||
|
'end': now.replace(day=1, month=now.month) - timezone.timedelta(days=1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'This year',
|
||||||
|
'start': now.replace(day=1, month=1),
|
||||||
|
'end': now,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
data = super().get_context_data(**kwargs)
|
data = super().get_context_data(**kwargs)
|
||||||
data["start_date"] = self.get_start_date()
|
data["start_date"] = self.get_start_date()
|
||||||
data["end_date"] = self.get_end_date()
|
data["end_date"] = self.get_end_date()
|
||||||
|
data["date_ranges"] = self.get_date_ranges()
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|||||||
@@ -11,10 +11,23 @@
|
|||||||
max-height: 400px;
|
max-height: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.force-limited-height {
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.rf {
|
.rf {
|
||||||
text-align: right !important;
|
text-align: right !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-card .apexcharts-svg {
|
||||||
|
border-radius: 0 0 var(--border-radius-lg, 0.5rem) var(--border-radius-lg, 0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-w-0 {
|
||||||
|
max-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--color-neutral-000: white;
|
--color-neutral-000: white;
|
||||||
--color-neutral-50: #F8FAFC;
|
--color-neutral-50: #F8FAFC;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div>
|
<div class="flex-1 truncate">
|
||||||
<h4 class="heading">{% block page_title %}{% endblock %}</h4>
|
<h4 class="heading truncate">{% block page_title %}{% endblock %}</h4>
|
||||||
</div>
|
</div>
|
||||||
<hr class="sep">
|
<hr class="sep">
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|||||||
@@ -10,31 +10,36 @@
|
|||||||
{% include 'a17t/includes/head.html' %}
|
{% include 'a17t/includes/head.html' %}
|
||||||
<link rel="icon" type="image/png" href="{% static 'dashboard/images/icon.png' %}">
|
<link rel="icon" type="image/png" href="{% static 'dashboard/images/icon.png' %}">
|
||||||
<script src="{% static 'apexcharts/dist/apexcharts.min.js'%}"></script>
|
<script src="{% static 'apexcharts/dist/apexcharts.min.js'%}"></script>
|
||||||
<script src="{% static 'litepicker/dist/js/main.js' %}"></script>
|
<script src="{% static 'litepicker/dist/nocss/litepicker.js' %}"></script>
|
||||||
|
<script src="{% static 'litepicker/dist/plugins/ranges.js' %}"></script>
|
||||||
|
<script src="{% static 'd3/d3.min.js' %}"></script>
|
||||||
|
<script src="{% static 'topojson/build/topojson.min.js' %}"></script>
|
||||||
|
<script src="{% static 'datamaps/dist/datamaps.world.min.js' %}"></script>
|
||||||
<script src="{% static 'dashboard/js/base.js' %}"></script>
|
<script src="{% static 'dashboard/js/base.js' %}"></script>
|
||||||
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
|
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
|
||||||
<link rel="stylesheet" href="{% static 'flag-icon-css/css/flag-icon.min.css' %}">
|
<link rel="stylesheet" href="{% static 'flag-icon-css/css/flag-icon.min.css' %}">
|
||||||
|
<link rel="stylesheet" href="{% static 'litepicker/dist/css/litepicker.css' %}">
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-neutral-100 min-h-full">
|
<body class="bg-neutral-100 min-h-full overflow-x-hidden">
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
|
||||||
<section class="max-w-screen-xl mx-auto px-4 py-4 md:py-12 md:flex">
|
<section class="max-w-screen-xl mx-auto px-4 py-4 md:py-12 md:flex">
|
||||||
<aside
|
<aside
|
||||||
class="mb-8 md:w-2/12 md:pr-6 relative flex flex-wrap md:block justify-between items-center overflow-x-hidden">
|
class="mb-2 md:w-2/12 md:pr-6 relative flex flex-wrap md:block justify-between items-center overflow-x-hidden">
|
||||||
<a class="icon ~urge ml-2 md:ml-6 md:mb-8 md:mt-3" href="{% url 'dashboard:dashboard' %}">
|
<a class="icon ~urge ml-2 md:ml-6 md:mb-8 md:mt-3" href="{% url 'dashboard:dashboard' %}">
|
||||||
<i class="fas fa-binoculars fa-3x text-urge-600 hidden md:block"></i>
|
<i class="fas fa-binoculars fa-3x text-urge-600 hidden md:block"></i>
|
||||||
<i class="fas fa-binoculars fa-2x text-urge-600 md:hidden"></i>
|
<i class="fas fa-binoculars fa-2x text-urge-600 md:hidden"></i>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button class="button ~neutral !low md:hidden"
|
<a tabindex="0" role="button" class="button ~neutral !low md:hidden"
|
||||||
onclick="document.getElementById('navMenuExpanded').classList.toggle('hidden')">
|
onclick="document.getElementById('navMenuExpanded').classList.toggle('hidden')">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-bars"></i>
|
<i class="fas fa-bars"></i>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</a>
|
||||||
<hr class="sep h-4 md:h-8 w-full">
|
<hr class="sep h-4 md:h-8 w-full">
|
||||||
<div id="navMenuExpanded"
|
<div id="navMenuExpanded"
|
||||||
class="bg-neutral-000 shadow-lg md:shadow-none p-4 hidden rounded-lg md:block md:bg-transparent md:border-none md:p-0 w-full">
|
class="bg-neutral-000 shadow-lg md:shadow-none p-4 hidden rounded-lg md:block md:bg-transparent md:border-none md:p-0 w-full">
|
||||||
@@ -43,7 +48,7 @@
|
|||||||
|
|
||||||
{% for service in user.owning_services.all %}
|
{% for service in user.owning_services.all %}
|
||||||
{% contextual_url 'dashboard:service' service.uuid as url %}
|
{% contextual_url 'dashboard:service' service.uuid as url %}
|
||||||
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name|truncatechars:16 url=url icon=service.link|iconify %}
|
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name url=url icon=service.link|iconify %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -62,7 +67,7 @@
|
|||||||
|
|
||||||
{% for service in user.collaborating_services.all %}
|
{% for service in user.collaborating_services.all %}
|
||||||
{% contextual_url 'dashboard:service' service.uuid as url %}
|
{% contextual_url 'dashboard:service' service.uuid as url %}
|
||||||
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name|truncatechars:20 url=url %}
|
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name url=url %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
<hr class="sep h-8">
|
<hr class="sep h-8">
|
||||||
|
|||||||
6
shynet/dashboard/templates/dashboard/includes/bar.html
Normal file
6
shynet/dashboard/templates/dashboard/includes/bar.html
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
<div
|
||||||
|
class="absolute h-6 rounded-md"
|
||||||
|
style="width: {% bar_width count max total %}; top: 6px; left: 0px; height: calc(100% - 12px); background-color: var(--color-urge-100-fallback)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
@@ -2,32 +2,48 @@
|
|||||||
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
|
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
|
||||||
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
|
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
|
||||||
</form>
|
</form>
|
||||||
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral bg-neutral-000 cursor-pointer w-auto" readonly>
|
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral bg-neutral-000 cursor-pointer" style="max-width: 200px;" readonly>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--litepickerMonthButtonHover: var(--color-urge);
|
--litepicker-button-prev-month-color-hover: var(--color-urge);
|
||||||
--litepickerDayColorHover: var(--color-urge);
|
--litepicker-button-next-month-color-hover: var(--color-urge);
|
||||||
--litepickerDayIsTodayColor: var(--color-urge);
|
--litepicker-day-color-hover: var(--color-urge);
|
||||||
--litepickerDayIsInRange: var(--color-urge-normal-fill);
|
--litepicker-is-today-color: var(--color-urge);
|
||||||
--litepickerDayIsStartBg: var(--color-urge);
|
--litepicker-is-in-range-color: var(--color-urge-normal-fill);
|
||||||
--litepickerDayIsEndBg: var(--color-urge);
|
--litepicker-is-start-color-bg: var(--color-urge);
|
||||||
--litepickerButtonApplyBg: var(--color-urge);
|
--litepicker-is-end-color-bg: var(--color-urge);
|
||||||
|
--litepicker-button-apply-color-bg: var(--color-urge);
|
||||||
|
}
|
||||||
|
|
||||||
|
.litepicker .container__predefined-ranges, .litepicker .container__months {
|
||||||
|
box-shadow: var(--fallback-box-shadow-normal) !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
var picker = new Litepicker({
|
var picker = new Litepicker({
|
||||||
element: document.getElementById('rangePicker'),
|
element: document.getElementById('rangePicker'),
|
||||||
|
plugins: ['ranges'],
|
||||||
singleMode: false,
|
singleMode: false,
|
||||||
format: "MMM DD 'YY",
|
format: "MMM DD 'YY",
|
||||||
maxDate: new Date(),
|
maxDate: new Date(),
|
||||||
startDate: Date.parse(document.getElementById("startDate").getAttribute("value")),
|
startDate: Date.parse(document.getElementById("startDate").getAttribute("value")),
|
||||||
endDate: Date.parse(document.getElementById("endDate").getAttribute("value")),
|
endDate: Date.parse(document.getElementById("endDate").getAttribute("value")),
|
||||||
onSelect: function (startDate, endDate) {
|
ranges: {
|
||||||
|
customRanges: {
|
||||||
|
{% for date_range in date_ranges %}
|
||||||
|
'{{ date_range.name }}': [
|
||||||
|
new Date('{{ date_range.start.isoformat }}'),
|
||||||
|
new Date('{{ date_range.end.isoformat }}')
|
||||||
|
],
|
||||||
|
{% endfor %}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
picker.on('selected', (startDate, endDate) => {
|
||||||
document.getElementById("startDate").setAttribute("value", startDate.getFullYear() +
|
document.getElementById("startDate").setAttribute("value", startDate.getFullYear() +
|
||||||
"-" + (startDate.getMonth() + 1) + "-" + startDate.getDate());
|
"-" + (startDate.getMonth() + 1) + "-" + startDate.getDate());
|
||||||
document.getElementById("endDate").setAttribute("value", endDate.getFullYear() + "-" +
|
document.getElementById("endDate").setAttribute("value", endDate.getFullYear() + "-" +
|
||||||
(endDate.getMonth() + 1) + "-" + endDate.getDate());
|
(endDate.getMonth() + 1) + "-" + endDate.getDate());
|
||||||
document.getElementById("datePicker").submit();
|
document.getElementById("datePicker").submit();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
59
shynet/dashboard/templates/dashboard/includes/map_chart.html
Normal file
59
shynet/dashboard/templates/dashboard/includes/map_chart.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{% load helpers %}
|
||||||
|
|
||||||
|
<div id="map-chart" class="relative"></div>
|
||||||
|
<script>
|
||||||
|
// Colors
|
||||||
|
const lightBlue = "#C4B5FD";
|
||||||
|
const highlightBlue = "#8B5CF6";
|
||||||
|
const white = "#ffffff";
|
||||||
|
|
||||||
|
// Data maps
|
||||||
|
const countryMapData = {};
|
||||||
|
const countryMapColors = {};
|
||||||
|
const countryMap = {
|
||||||
|
{% for country in countries %}"{{country.country|safe|datamap_id}}": {{country.count}},
|
||||||
|
{% endfor %}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Max session count will be full opacity
|
||||||
|
const maxSessionCount = Math.max(...Object.values(countryMap));
|
||||||
|
|
||||||
|
// Color scale starts from opacity 0.1 - 1.0, 0 sessions gets opacity 0
|
||||||
|
const minPercentage = 0.1
|
||||||
|
|
||||||
|
// Loop over country map and transform data for Datamaps use
|
||||||
|
const keys = Object.keys(countryMap);
|
||||||
|
const length = keys.length;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
countryMapData[keys[i]] = {
|
||||||
|
sessionCount: countryMap[keys[i]],
|
||||||
|
color: `rgba(124, 58, 237, ${countryMap[keys[i]] === 0 ? 0 : minPercentage + (countryMap[keys[i]] / maxSessionCount * (1 - minPercentage))})`
|
||||||
|
};
|
||||||
|
countryMapColors[keys[i]] = countryMapData[keys[i]].color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create datamap
|
||||||
|
const map = new Datamap({
|
||||||
|
element: document.getElementById('map-chart'),
|
||||||
|
projection: 'mercator',
|
||||||
|
responsive: true,
|
||||||
|
geographyConfig: {
|
||||||
|
borderColor: lightBlue,
|
||||||
|
highlightBorderColor: highlightBlue,
|
||||||
|
highlightBorderWidth: 1.5,
|
||||||
|
highlightFillColor: (geography) => geography.color || white,
|
||||||
|
highlightFillOpacity: 0.9,
|
||||||
|
popupTemplate: (geography, data) => '<div class="hoverinfo"><strong>' + geography.properties.name + '</strong>: ' + data.sessionCount + ' sessions</div>'
|
||||||
|
|
||||||
|
},
|
||||||
|
fills: {
|
||||||
|
defaultFill: white
|
||||||
|
},
|
||||||
|
data: countryMapData,
|
||||||
|
aspectRatio: 0.68
|
||||||
|
});
|
||||||
|
map.updateChoropleth(countryMapColors);
|
||||||
|
|
||||||
|
// Handle resize. TODO: debounce?
|
||||||
|
window.onresize = () => map.resize();
|
||||||
|
</script>
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
{% load humanize helpers %}
|
{% load humanize helpers %}
|
||||||
|
|
||||||
<a class="card ~neutral !low service mb-6 p-0" href="{% contextual_url 'dashboard:service' object.uuid %}">
|
<a class="card chart-card overflow-visible ~neutral !low service mb-6 p-0" href="{% contextual_url 'dashboard:service' object.uuid %}">
|
||||||
{% with stats=object.stats %}
|
{% with stats=object.stats %}
|
||||||
<div class="p-4 md:flex justify-between">
|
<div class="p-4 md:flex justify-between overflow-none">
|
||||||
<div class="flex items-center mb-4 md:mb-0">
|
<div class="flex items-center mb-4 md:mb-0 md:flex-1 md:min-w-0 truncate pr-0 md:pr-2">
|
||||||
<h3 class="heading text-xl md:text-2xl mr-2 mb-1 text-urge-600 flex items-center">
|
<h3 class="heading text-xl md:text-2xl mr-2 mb-1 text-urge-600 flex items-center truncate" title="{{object.name}}">
|
||||||
{{object.link|iconify}}
|
{{object.link|iconify}}
|
||||||
<span>{{object.name}}</span>
|
<span class="truncate">{{object.name}}</span>
|
||||||
</h3>
|
</h3>
|
||||||
{% include 'dashboard/includes/stats_status_chip.html' %}
|
{% include 'dashboard/includes/stats_status_chip.html' %}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-3 lg:gap-6 md:flex-none">
|
||||||
<div>
|
<div>
|
||||||
<p>Sessions</p>
|
<p>Sessions</p>
|
||||||
<p class="label">
|
<p class="label">
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr class="sep h-4">
|
<hr class="sep h-4">
|
||||||
<div style="bottom: -1px;">
|
<div style="bottom: -1px;">
|
||||||
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data sparkline=True height=100 name=object.uuid %}
|
{% include 'dashboard/includes/time_chart.html' with data=stats.chart_data sparkline=True height=100 name=object.uuid tooltip_format=stats.chart_tooltip_format granularity=stats.chart_granularity %}
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</a>
|
</a>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{% load helpers %}
|
{% load helpers %}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-neutral-100{% endif %} flex items-center"
|
<a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-neutral-100{% endif %} flex items-center" title="{{label}}"
|
||||||
{% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{icon}} {{label}}</a>
|
{% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{icon}} <span class="truncate">{{label}}</span></a>
|
||||||
</div>
|
</div>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{% with stats=object.get_daily_stats %}
|
{% with stats=object.get_daily_stats %}
|
||||||
{% if stats.currently_online > 0 %}
|
{% if stats.currently_online > 0 %}
|
||||||
<span class="chip ~positive !high">
|
<span class="chip ~positive !high whitespace-nowrap">
|
||||||
{{stats.currently_online|intcomma}} online
|
{{stats.currently_online|intcomma}} online
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -5,9 +5,14 @@
|
|||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
shared: false,
|
shared: true,
|
||||||
|
x: {
|
||||||
|
format: '{{tooltip_format|default:"MMM d"}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
},
|
},
|
||||||
colors: ["#805AD5"],
|
|
||||||
chart: {
|
chart: {
|
||||||
zoom: {
|
zoom: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
@@ -15,7 +20,7 @@
|
|||||||
toolbar: {
|
toolbar: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
type: 'area',
|
type: 'line',
|
||||||
height: {{height|default:"200"}},
|
height: {{height|default:"200"}},
|
||||||
offsetY: -1,
|
offsetY: -1,
|
||||||
animations: {
|
animations: {
|
||||||
@@ -24,16 +29,14 @@
|
|||||||
sparkline: {
|
sparkline: {
|
||||||
enabled: {% if sparkline %}true{% else %}false{% endif %},
|
enabled: {% if sparkline %}true{% else %}false{% endif %},
|
||||||
},
|
},
|
||||||
fill: {
|
{% if granularity == "daily" and click_zoom %}
|
||||||
type: 'gradient',
|
events: {
|
||||||
gradient: {
|
markerClick: function(event, chartContext, { seriesIndex, dataPointIndex, w: {config}}) {
|
||||||
shadeIntensity: 1,
|
const day = config.labels[dataPointIndex]
|
||||||
inverseColors: false,
|
window.location.href = `?startDate=${day}&endDate=${day}`
|
||||||
opacityFrom: 0.8,
|
|
||||||
opacityTo: 0,
|
|
||||||
stops: [0, 75, 100]
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{% endif %}
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
padding: {
|
padding: {
|
||||||
@@ -63,14 +66,26 @@
|
|||||||
},
|
},
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: "datetime",
|
type: "datetime",
|
||||||
|
labels: {
|
||||||
|
datetimeUTC: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
stroke: {
|
stroke: {
|
||||||
width: 1.5,
|
width: 2,
|
||||||
|
curve: 'smooth',
|
||||||
},
|
},
|
||||||
series: [{
|
series: [{
|
||||||
name: "{{unit|default:'Sessions'}}",
|
name: "Hits",
|
||||||
data: {{data|safe}}
|
type: 'area',
|
||||||
}]
|
color: "#ddd6fe",
|
||||||
|
data: {{data.hits|safe}}
|
||||||
|
}, {
|
||||||
|
name: "Sessions",
|
||||||
|
type: 'line',
|
||||||
|
color: "#805AD5",
|
||||||
|
data: {{data.sessions|safe}}
|
||||||
|
}],
|
||||||
|
labels: {{data.labels|safe}}
|
||||||
};
|
};
|
||||||
var triggerMatchesChart = new ApexCharts(document.querySelector("#chart{{name|default:'Main'}}"), triggerMatchesChartOptions);
|
var triggerMatchesChart = new ApexCharts(document.querySelector("#chart{{name|default:'Main'}}"), triggerMatchesChartOptions);
|
||||||
triggerMatchesChart.render();
|
triggerMatchesChart.render();
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="md:flex justify-between items-center">
|
<div class="md:flex justify-between items-center">
|
||||||
<div>
|
<div class="flex-1 truncate display-none md:display-block mr-4 md:mb-0 mb-4" title="{{request.site.name|default:"Dashboard"}}">
|
||||||
<h4 class="heading">{{request.site.name|default:"Dashboard"}}</h4>
|
<h4 class="heading truncate">{{request.site.name|default:"Dashboard"}}</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="mr-1">
|
<div class="mr-1">
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="sep">
|
<hr class="sep h-8 md:h-12">
|
||||||
{% for object in object_list|dictsortreversed:"stats.session_count" %}
|
{% for object in object_list|dictsortreversed:"stats.session_count" %}
|
||||||
{% include 'dashboard/includes/service_overview.html' %}
|
{% include 'dashboard/includes/service_overview.html' %}
|
||||||
{% empty %}
|
{% empty %}
|
||||||
|
|||||||
@@ -93,10 +93,10 @@
|
|||||||
</article>
|
</article>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
<div class="card overflow-visible ~neutral !low py-0 mb-6">
|
||||||
<div class="card ~neutral !low py-0 mb-6">
|
{% include 'dashboard/includes/time_chart.html' with data=stats.chart_data tooltip_format=stats.chart_tooltip_format granularity=stats.chart_granularity click_zoom=True %}
|
||||||
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data %}
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
<div class="card ~neutral !low limited-height py-2">
|
<div class="card ~neutral !low limited-height py-2">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
@@ -109,8 +109,20 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for location in stats.locations %}
|
{% for location in stats.locations %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{location.location|default:"Unknown"|urldisplay}}</td>
|
<td class="truncate w-full max-w-0 relative">
|
||||||
<td class="rf">{{location.count|intcomma}}</td>
|
{% include 'dashboard/includes/bar.html' with count=location.count max=stats.locations.0.count total=stats.hit_count %}
|
||||||
|
<div class="relative flex items-center">
|
||||||
|
{{location.location|default:"Unknown"|urldisplay}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex justify-end items-center">
|
||||||
|
{{location.count|intcomma}}
|
||||||
|
<span class="text-xs rf" style="min-width: 48px">
|
||||||
|
({{location.count|percent:stats.hit_count}})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -120,6 +132,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card ~neutral !low force-limited-height py-2 overflow-y-hidden">
|
||||||
|
<p class="text-sm font-semibold mx-2 p-2 border-b mb-2">Sessions by Geography</p>
|
||||||
|
{% include 'dashboard/includes/map_chart.html' with countries=stats.countries %}
|
||||||
|
</div>
|
||||||
<div class="card ~neutral !low limited-height py-2">
|
<div class="card ~neutral !low limited-height py-2">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead class="text-sm">
|
<thead class="text-sm">
|
||||||
@@ -131,30 +147,20 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for referrer in stats.referrers %}
|
{% for referrer in stats.referrers %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{referrer.referrer|default:"Direct"|urldisplay}}</td>
|
<td class="truncate w-full max-w-0 relative">
|
||||||
<td class="rf">{{referrer.count|intcomma}}</td>
|
{% include 'dashboard/includes/bar.html' with count=referrer.count max=stats.referrers.0.count total=stats.session_count %}
|
||||||
</tr>
|
<div class="relative flex items-center">
|
||||||
{% empty %}
|
{{referrer.referrer|default:"Direct"|urldisplay}}
|
||||||
<tr>
|
|
||||||
<td><span class="text-gray-600">No data yet...</span></td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card ~neutral !low limited-height py-2">
|
</td>
|
||||||
<table class="table">
|
<td>
|
||||||
<thead class="text-sm">
|
<div class="flex justify-end items-center">
|
||||||
<tr>
|
{{referrer.count|intcomma}}
|
||||||
<th>Country</th>
|
<span class="text-xs rf" style="min-width: 48px">
|
||||||
<th class="rf">Sessions</th>
|
({{referrer.count|percent:stats.session_count}})
|
||||||
</tr>
|
</span>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
</td>
|
||||||
{% for country in stats.countries %}
|
|
||||||
<tr>
|
|
||||||
<td><span class="{{country.country|flag_class}}"></span></span>{{country.country|country_name}}</td>
|
|
||||||
<td class="rf">{{country.count|intcomma}}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -175,8 +181,20 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for os in stats.operating_systems %}
|
{% for os in stats.operating_systems %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="flex items-center">{{os.os|iconify}}<span>{{os.os|default:"Unknown"}}</span></td>
|
<td class="flex items-center truncate w-full max-w-0 relative" title="{{os.os|default:'Unknown'}}">
|
||||||
<td class="rf">{{os.count|intcomma}}</td>
|
{% include 'dashboard/includes/bar.html' with count=os.count max=stats.operating_systems.0.count total=stats.session_count %}
|
||||||
|
<div class="relative flex items-center">
|
||||||
|
{{os.os|iconify}}<span>{{os.os|default:"Unknown"}}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex justify-end items-center">
|
||||||
|
{{os.count|intcomma}}
|
||||||
|
<span class="text-xs rf" style="min-width: 48px">
|
||||||
|
({{os.count|percent:stats.session_count}})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -197,9 +215,21 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for browser in stats.browsers %}
|
{% for browser in stats.browsers %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="flex items-center">
|
<td class="flex items-center truncate w-full max-w-0 relative" title="{{browser.browser|default:'Unknown'}}">
|
||||||
{{browser.browser|iconify}}<span>{{browser.browser|default:"Unknown"}}</span></td>
|
{% include 'dashboard/includes/bar.html' with count=browser.count max=stats.browsers.0.count total=stats.session_count %}
|
||||||
<td class="rf">{{browser.count|intcomma}}</td>
|
</div>
|
||||||
|
<div class="relative flex items-center">
|
||||||
|
{{browser.browser|iconify}}<span>{{browser.browser|default:"Unknown"}}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex justify-end items-center">
|
||||||
|
{{browser.count|intcomma}}
|
||||||
|
<span class="text-xs rf" style="min-width: 48px">
|
||||||
|
({{browser.count|percent:stats.session_count}})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -220,8 +250,20 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
{% for device_type in stats.device_types %}
|
{% for device_type in stats.device_types %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{device_type.device_type|default:"Unknown"|title}}</td>
|
<td class="truncate w-full max-w-0 relative">
|
||||||
<td class="rf">{{device_type.count|intcomma}}</td>
|
{% include 'dashboard/includes/bar.html' with count=device_type.count max=stats.device_types.0.count total=stats.session_count %}
|
||||||
|
<div class="relative flex items-center">
|
||||||
|
{{device_type.device_type|default:"Unknown"|title}}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="flex justify-end items-center">
|
||||||
|
{{device_type.count|intcomma}}
|
||||||
|
<span class="text-xs rf" style="min-width: 48px">
|
||||||
|
({{device_type.count|percent:stats.session_count}})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -232,9 +274,9 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card ~neutral !low limited-height py-2">
|
<div class="card ~neutral !low py-2">
|
||||||
{% include 'dashboard/includes/session_list.html' %}
|
{% include 'dashboard/includes/session_list.html' %}
|
||||||
<hr class="sep h-8">
|
<hr class="sep h-8 md:h-12">
|
||||||
<a href="{% contextual_url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more
|
<a href="{% contextual_url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more
|
||||||
sessions
|
sessions
|
||||||
→</a>
|
→</a>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
{{session.last_seen|date:"g:i a"}}{% if session.is_currently_active %} <span class="chip ~positive !high text-base">Online</span>{% endif %}</p>
|
{{session.last_seen|date:"g:i a"}}{% if session.is_currently_active %} <span class="chip ~positive !high text-base">Online</span>{% endif %}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="sep h-8">
|
<hr class="sep h-8 md:h-12">
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-gray-400 font-medium">
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-gray-400 font-medium">
|
||||||
<div>
|
<div>
|
||||||
<p>Browser</p>
|
<p>Browser</p>
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<p>Location</p>
|
<p>Location</p>
|
||||||
<p class="label">
|
<p class="label">
|
||||||
{% if session.latitude %}
|
{% if session.latitude %}
|
||||||
<a href="https://www.google.com/maps/search/?api=1&query={{session.latitude}},{{session.longitude}}">Open
|
<a href="{{session|location_url}}" target="_blank">Open
|
||||||
in Maps ↗</a>
|
in Maps ↗</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
Unknown
|
Unknown
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="md:flex card ~neutral !low flex-grow justify-between">
|
<div class="md:flex card ~neutral !low flex-grow justify-between">
|
||||||
<div class="mb-4 md:mb-0 md:w-1/2">
|
<div class="mb-4 md:mb-0 md:w-1/2">
|
||||||
<p class="label font-medium text-lg">{{hit.location|default:"Unknown"|urlize}}</p>
|
<p class="label font-medium text-lg truncate">{{hit.location|default:"Unknown"|urlize}}</p>
|
||||||
{% if hit.referrer %}
|
{% if hit.referrer %}
|
||||||
<p>via {{hit.referrer|urlize}}<p>
|
<p>via {{hit.referrer|urlize}}<p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block service_content %}
|
{% block service_content %}
|
||||||
<div class="card ~neutral !low mb-8 pt-2 max-w-full overflow-x-scroll">
|
<div class="card ~neutral !low mb-8 pt-2 max-w-full overflow-x-auto">
|
||||||
{% include 'dashboard/includes/session_list.html' %}
|
{% include 'dashboard/includes/session_list.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% pagination page_obj request %}
|
{% pagination page_obj request %}
|
||||||
|
|||||||
@@ -6,21 +6,21 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="md:flex justify-between items-center" id="heading">
|
<div class="md:flex justify-between items-center" id="heading">
|
||||||
<a class="flex items-center mb-4 md:mb-0" href="{% contextual_url 'dashboard:service' object.uuid %}">
|
<a class="flex items-center mb-4 md:mb-0 truncate" href="{% contextual_url 'dashboard:service' object.uuid %}">
|
||||||
<h3 class="heading leading-none mr-4">
|
<h3 class="heading items-center mr-4 md:mr-2 flex truncate">
|
||||||
{{object.link|iconify}}
|
{{object.link|iconify}}
|
||||||
{{object.name}}
|
<span class="flex-1 truncate ml-2" title="{{object.name}}">{{object.name}}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class='text-3xl'>
|
<div class="text-3xl md:mr-2">
|
||||||
{% include 'dashboard/includes/stats_status_chip.html' %}
|
{% include 'dashboard/includes/stats_status_chip.html' %}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center flex-none">
|
||||||
{% block service_actions %}
|
{% block service_actions %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<hr class="sep h-8">
|
<hr class="sep h-8 md:h-12">
|
||||||
{% block service_content %}
|
{% block service_content %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -43,6 +43,14 @@ def country_name(isocode):
|
|||||||
return "Unknown"
|
return "Unknown"
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def datamap_id(isocode):
|
||||||
|
try:
|
||||||
|
return pycountry.countries.get(alpha_2=isocode).alpha_3
|
||||||
|
except:
|
||||||
|
return "UNKNOWN"
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag
|
@register.simple_tag
|
||||||
def relative_stat_tone(
|
def relative_stat_tone(
|
||||||
start,
|
start,
|
||||||
@@ -172,7 +180,7 @@ def iconify(text):
|
|||||||
domain = text + ".com"
|
domain = text + ".com"
|
||||||
|
|
||||||
return SafeString(
|
return SafeString(
|
||||||
f'<span class="icon mr-1"><img src="https://icons.duckduckgo.com/ip3/{domain}.ico"></span>'
|
f'<span class="icon mr-1 flex-none"><img src="https://icons.duckduckgo.com/ip3/{domain}.ico"></span>'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -181,11 +189,12 @@ def urldisplay(url):
|
|||||||
if url.startswith("http"):
|
if url.startswith("http"):
|
||||||
display_url = url.replace("http://", "").replace("https://", "")
|
display_url = url.replace("http://", "").replace("https://", "")
|
||||||
return SafeString(
|
return SafeString(
|
||||||
f"<a href='{url}' title='{url}' rel='nofollow' class='flex items-center'>{iconify(url)} {escape(display_url if len(display_url) < 40 else display_url[:40] + '...')}</a>"
|
f"<a href='{url}' title='{url}' rel='nofollow' class='flex items-center mr-1'>{iconify(url)}<span class='truncate'>{escape(display_url)}</span></a>"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
class ContextualURLNode(template.Node):
|
class ContextualURLNode(template.Node):
|
||||||
"""Extension of the Django URLNode to support including contextual parameters in URL outputs. In other words, URLs generated will keep the start and end date parameters."""
|
"""Extension of the Django URLNode to support including contextual parameters in URL outputs. In other words, URLs generated will keep the start and end date parameters."""
|
||||||
|
|
||||||
@@ -205,9 +214,13 @@ class ContextualURLNode(template.Node):
|
|||||||
url_parts = list(urlparse(url))
|
url_parts = list(urlparse(url))
|
||||||
query = dict(urllib.parse.parse_qsl(url_parts[4]))
|
query = dict(urllib.parse.parse_qsl(url_parts[4]))
|
||||||
|
|
||||||
query.update({
|
query.update(
|
||||||
param: context.request.GET.get(param) for param in self.CONTEXT_PARAMS if param in context.request.GET and param not in query
|
{
|
||||||
})
|
param: context.request.GET.get(param)
|
||||||
|
for param in self.CONTEXT_PARAMS
|
||||||
|
if param in context.request.GET and param not in query
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
url_parts[4] = urllib.parse.urlencode(query)
|
url_parts[4] = urllib.parse.urlencode(query)
|
||||||
|
|
||||||
@@ -225,3 +238,38 @@ def contextual_url(*args, **kwargs):
|
|||||||
urlnode = url_tag(*args, **kwargs)
|
urlnode = url_tag(*args, **kwargs)
|
||||||
return ContextualURLNode(urlnode)
|
return ContextualURLNode(urlnode)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def location_url(session):
|
||||||
|
return settings.LOCATION_URL.replace("$LATITUDE", str(session.latitude)).replace(
|
||||||
|
"$LONGITUDE", str(session.longitude)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter
|
||||||
|
def percent(value, total):
|
||||||
|
if total == 0:
|
||||||
|
return "N/A"
|
||||||
|
|
||||||
|
percent = value / total
|
||||||
|
|
||||||
|
if percent < 0.001:
|
||||||
|
return "<0.1%"
|
||||||
|
|
||||||
|
return f'{percent:.1%}'
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def bar_width(count, max, total):
|
||||||
|
if total == 0 or max == 0:
|
||||||
|
return "0"
|
||||||
|
|
||||||
|
if settings.USE_RELATIVE_MAX_IN_BAR_VISUALIZATION:
|
||||||
|
percent = count / max
|
||||||
|
else:
|
||||||
|
percent = count / total
|
||||||
|
|
||||||
|
if percent < 0.001:
|
||||||
|
return "0"
|
||||||
|
|
||||||
|
return f'{percent:.1%}'
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from .mixins import DateRangeMixin
|
|||||||
class DashboardView(LoginRequiredMixin, DateRangeMixin, ListView):
|
class DashboardView(LoginRequiredMixin, DateRangeMixin, ListView):
|
||||||
model = Service
|
model = Service
|
||||||
template_name = "dashboard/pages/dashboard.html"
|
template_name = "dashboard/pages/dashboard.html"
|
||||||
paginate_by = 5
|
paginate_by = settings.DASHBOARD_PAGE_SIZE
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Service.objects.filter(
|
return Service.objects.filter(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import urllib.parse as urlparse
|
|||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
|
|
||||||
# Increment on new releases
|
# Increment on new releases
|
||||||
VERSION = "v0.8.2"
|
VERSION = "v0.10.0"
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
@@ -301,12 +301,22 @@ NPM_ROOT_PATH = "../"
|
|||||||
NPM_FILE_PATTERNS = {
|
NPM_FILE_PATTERNS = {
|
||||||
"a17t": [os.path.join("dist", "a17t.css"), os.path.join("dist", "tailwind.css")],
|
"a17t": [os.path.join("dist", "a17t.css"), os.path.join("dist", "tailwind.css")],
|
||||||
"apexcharts": [os.path.join("dist", "apexcharts.min.js")],
|
"apexcharts": [os.path.join("dist", "apexcharts.min.js")],
|
||||||
"litepicker": [os.path.join("dist", "js", "main.js")],
|
"litepicker": [
|
||||||
|
os.path.join("dist", "nocss", "litepicker.js"),
|
||||||
|
os.path.join("dist", "css", "litepicker.css"),
|
||||||
|
os.path.join("dist", "plugins", "ranges.js"),
|
||||||
|
],
|
||||||
"turbolinks": [os.path.join("dist", "turbolinks.js")],
|
"turbolinks": [os.path.join("dist", "turbolinks.js")],
|
||||||
"stimulus": [os.path.join("dist", "stimulus.umd.js")],
|
"stimulus": [os.path.join("dist", "stimulus.umd.js")],
|
||||||
"inter-ui": [os.path.join("Inter (web)", "*")],
|
"inter-ui": [os.path.join("Inter (web)", "*")],
|
||||||
"@fortawesome": [os.path.join("fontawesome-free", "js", "all.min.js")],
|
"@fortawesome": [os.path.join("fontawesome-free", "js", "all.min.js")],
|
||||||
"flag-icon-css": [os.path.join("css", "flag-icon.min.css"), os.path.join("flags", "*")],
|
"datamaps": [os.path.join("dist", "datamaps.world.min.js")],
|
||||||
|
"d3": ["d3.min.js"],
|
||||||
|
"topojson": [os.path.join("build", "topojson.min.js")],
|
||||||
|
"flag-icon-css": [
|
||||||
|
os.path.join("css", "flag-icon.min.css"),
|
||||||
|
os.path.join("flags", "*"),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
# Shynet
|
# Shynet
|
||||||
@@ -340,3 +350,16 @@ BLOCK_ALL_IPS = os.getenv("BLOCK_ALL_IPS", "False") == "True"
|
|||||||
|
|
||||||
# Include date and service ID in salt?
|
# Include date and service ID in salt?
|
||||||
AGGRESSIVE_HASH_SALTING = os.getenv("AGGRESSIVE_HASH_SALTING", "False") == "True"
|
AGGRESSIVE_HASH_SALTING = os.getenv("AGGRESSIVE_HASH_SALTING", "False") == "True"
|
||||||
|
|
||||||
|
# What location url should be linked to in the frontend?
|
||||||
|
LOCATION_URL = os.getenv(
|
||||||
|
"LOCATION_URL", "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE"
|
||||||
|
)
|
||||||
|
|
||||||
|
# How many services should be displayed on dashboard page?
|
||||||
|
DASHBOARD_PAGE_SIZE = int(os.getenv("DASHBOARD_PAGE_SIZE", "5"))
|
||||||
|
|
||||||
|
# Should background bars be scaled to full width?
|
||||||
|
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION = (
|
||||||
|
os.getenv("USE_RELATIVE_MAX_IN_BAR_VISUALIZATION", "True") == "True"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user