Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
5c782ddb7d | ||
|
|
a6a508899a | ||
|
|
2b003b8fa9 | ||
|
|
023e0fde15 | ||
|
|
a1a083a403 | ||
|
|
8b167b2c74 | ||
|
|
4cd0c4735d | ||
|
|
d9e1ffddb1 | ||
|
|
9fb875f749 | ||
|
|
f6e502dfbd | ||
|
|
7c69b0bd81 | ||
|
|
78bea501a8 | ||
|
|
c2daf3a5a5 | ||
|
|
df6786e037 | ||
|
|
6621625d90 | ||
|
|
32ae0aa5f3 | ||
|
|
2221a99662 | ||
|
|
69ec37331a | ||
|
|
03f88af03c | ||
|
|
87b7ce2edc | ||
|
|
26c1ae2bce | ||
|
|
36d72508e6 | ||
|
|
68945df17d | ||
|
|
fef531efa9 | ||
|
|
46176b19fc | ||
|
|
94c53d2ab5 | ||
|
|
ea893b2322 | ||
|
|
71431fcaa6 | ||
|
|
e9536f1816 | ||
|
|
6f835a4f27 | ||
|
|
faf4f48e75 | ||
|
|
278306daa4 | ||
|
|
2c0fafefea | ||
|
|
6eb41e016a | ||
|
|
369f4d8d6b | ||
|
|
3d43f223eb | ||
|
|
351efff147 | ||
|
|
6867cbd282 | ||
|
|
c03ef52ba8 | ||
|
|
9cb030ecbd | ||
|
|
8bab14cc8a |
46
.github/workflows/build-docker.yml
vendored
Normal file
46
.github/workflows/build-docker.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
name: Build 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
|
||||
WORKDIR /usr/src/shynet
|
||||
|
||||
1
Pipfile
1
Pipfile
@@ -13,7 +13,6 @@ django-ipware = "~=2.1.0"
|
||||
pyyaml = "~=5.4"
|
||||
ua-parser = "~=0.10.0"
|
||||
user-agents = "~=2.1"
|
||||
emoji-country-flag = "~=1.2.1"
|
||||
rules = "~=2.2"
|
||||
gunicorn = "~=20.0.4"
|
||||
psycopg2-binary = "~=2.8.5"
|
||||
|
||||
49
Pipfile.lock
generated
49
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "f8c76565a776f1bd36364077a86d6c16fccc522d9d2024bb9b51be5cb9f8b4b5"
|
||||
"sha256": "c51ea0205c9ffe753b9ef5249cd49c2338bb50768ae104113bfb7b97b5f9d70c"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -19,23 +19,21 @@
|
||||
"sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21",
|
||||
"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"
|
||||
},
|
||||
"asgiref": {
|
||||
"hashes": [
|
||||
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
|
||||
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
|
||||
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
|
||||
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==3.3.1"
|
||||
"version": "==3.3.4"
|
||||
},
|
||||
"billiard": {
|
||||
"hashes": [
|
||||
"sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede",
|
||||
"sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a"
|
||||
"sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547",
|
||||
"sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"
|
||||
],
|
||||
"version": "==3.6.3.0"
|
||||
"version": "==3.6.4.0"
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
@@ -57,7 +55,6 @@
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"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"
|
||||
},
|
||||
"defusedxml": {
|
||||
@@ -65,16 +62,15 @@
|
||||
"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
|
||||
"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"
|
||||
},
|
||||
"django": {
|
||||
"hashes": [
|
||||
"sha256:32ce792ee9b6a0cbbec340123e229ac9f765dff8c2a4ae9247a14b2ba3a365a7",
|
||||
"sha256:baf099db36ad31f970775d0be5587cc58a6256a6771a44eb795b554d45f211b8"
|
||||
"sha256:c348b3ddc452bf4b62361f0752f71a339140c777ebea3cdaaaa8fdb7f417a862",
|
||||
"sha256:f8393103e15ec2d2d313ccbb95a3f1da092f9f58d74ac1c61ca2ac0436ae1eac"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.7"
|
||||
"version": "==3.1.8"
|
||||
},
|
||||
"django-allauth": {
|
||||
"hashes": [
|
||||
@@ -85,11 +81,11 @@
|
||||
},
|
||||
"django-debug-toolbar": {
|
||||
"hashes": [
|
||||
"sha256:84e2607d900dbd571df0a2acf380b47c088efb787dce9805aefeb407341961d2",
|
||||
"sha256:9e5a25d0c965f7e686f6a8ba23613ca9ca30184daa26487706d4829f5cfb697a"
|
||||
"sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33",
|
||||
"sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2"
|
||||
"version": "==3.2.1"
|
||||
},
|
||||
"django-health-check": {
|
||||
"hashes": [
|
||||
@@ -120,14 +116,6 @@
|
||||
"index": "pypi",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"emoji-country-flag": {
|
||||
"hashes": [
|
||||
"sha256:338f5e374119dcde093cfeaa8ca3af372d4b8d984d89a7fb2fb0db0011662560",
|
||||
"sha256:a3a068191294294143d8ef294fdfe9792c5c243753eac130798bf2fa5de38185"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.2.4"
|
||||
},
|
||||
"geoip2": {
|
||||
"hashes": [
|
||||
"sha256:5869e987bc54c0d707264fec4710661332cc38d2dca5a7f9bb5362d0308e2ce0",
|
||||
@@ -157,7 +145,6 @@
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.10"
|
||||
},
|
||||
"kombu": {
|
||||
@@ -165,14 +152,12 @@
|
||||
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
|
||||
"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"
|
||||
},
|
||||
"maxminddb": {
|
||||
"hashes": [
|
||||
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.0.3"
|
||||
},
|
||||
"oauthlib": {
|
||||
@@ -180,7 +165,6 @@
|
||||
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
||||
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"psycopg2-binary": {
|
||||
@@ -293,14 +277,12 @@
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"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"
|
||||
},
|
||||
"requests-oauthlib": {
|
||||
"hashes": [
|
||||
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
|
||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
|
||||
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
|
||||
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
@@ -316,7 +298,6 @@
|
||||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"ua-parser": {
|
||||
@@ -332,7 +313,6 @@
|
||||
"sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",
|
||||
"sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"
|
||||
],
|
||||
"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.4"
|
||||
},
|
||||
"user-agents": {
|
||||
@@ -348,7 +328,6 @@
|
||||
"sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
|
||||
"sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
|
||||
],
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"whitenoise": {
|
||||
|
||||
15
TEMPLATE.env
15
TEMPLATE.env
@@ -82,4 +82,17 @@ BLOCK_ALL_IPS=False
|
||||
# that IP collection is also disabled, and external keys (primary
|
||||
# keys) aren't supplied. It will also prevent sessions from spanning
|
||||
# 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
|
||||
|
||||
10
app.json
10
app.json
@@ -122,6 +122,16 @@
|
||||
"description": "Set to 'False' if you do not want the version to be displayed on the frontend.",
|
||||
"value": "True",
|
||||
"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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
172
package-lock.json
generated
172
package-lock.json
generated
@@ -1,12 +1,153 @@
|
||||
{
|
||||
"name": "shynet",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"packages": {
|
||||
"": {
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
"a17t": "^0.5.1",
|
||||
"apexcharts": "^3.24.0",
|
||||
"flag-icon-css": "^3.5.0",
|
||||
"inter-ui": "^3.15.0",
|
||||
"litepicker": "^2.0.11"
|
||||
}
|
||||
},
|
||||
"node_modules/@fortawesome/fontawesome-free": {
|
||||
"version": "5.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
|
||||
"integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/a17t": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/a17t/-/a17t-0.5.1.tgz",
|
||||
"integrity": "sha512-peIPrH9eDiu49LLzLlSTFFrXj6WLlEX3TRsUkqyyOHi/i58ilJ/eERnu7AcswXhuCBx+/2W9EUuHM+8iAq4ipg=="
|
||||
},
|
||||
"node_modules/apexcharts": {
|
||||
"version": "3.26.2",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.26.2.tgz",
|
||||
"integrity": "sha512-CD7bad4ygwc9rs9vOQDDagUcoJ1mcc9BwNSiQB14l6jiZBCQKrXxnG4I1ZjJ2MIel/Y5GmsJFs8HTcZBqpe/Ew==",
|
||||
"dependencies": {
|
||||
"svg.draggable.js": "^2.2.2",
|
||||
"svg.easing.js": "^2.0.0",
|
||||
"svg.filter.js": "^2.0.2",
|
||||
"svg.pathmorphing.js": "^0.1.3",
|
||||
"svg.resize.js": "^1.4.3",
|
||||
"svg.select.js": "^3.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/apexcharts/apexcharts.js?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/flag-icon-css": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/flag-icon-css/-/flag-icon-css-3.5.0.tgz",
|
||||
"integrity": "sha512-pgJnJLrtb0tcDgU1fzGaQXmR8h++nXvILJ+r5SmOXaaL/2pocunQo2a8TAXhjQnBpRLPtZ1KCz/TYpqeNuE2ew=="
|
||||
},
|
||||
"node_modules/inter-ui": {
|
||||
"version": "3.18.1",
|
||||
"resolved": "https://registry.npmjs.org/inter-ui/-/inter-ui-3.18.1.tgz",
|
||||
"integrity": "sha512-W3LnAirp6a1ixpAHZwr9gH52KlOQOAp0oqbmIoGi2dAIlcIB7auJgLr9XFHUzYy2FoZ0Nf7aPe/nHMZB4/Zvdg=="
|
||||
},
|
||||
"node_modules/litepicker": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/litepicker/-/litepicker-2.0.11.tgz",
|
||||
"integrity": "sha512-7MECMp2EDGIYDIz9QT24t9hWpgBD9JD57ZdDrbffNMGfbw0JVhBhvlYsyaIUuYhywtLvgmI5lfulM7XF2HLEkg=="
|
||||
},
|
||||
"node_modules/svg.draggable.js": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
|
||||
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
|
||||
"dependencies": {
|
||||
"svg.js": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svg.easing.js": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
|
||||
"integrity": "sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=",
|
||||
"dependencies": {
|
||||
"svg.js": ">=2.3.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svg.filter.js": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
|
||||
"integrity": "sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=",
|
||||
"dependencies": {
|
||||
"svg.js": "^2.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svg.js": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
|
||||
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
|
||||
},
|
||||
"node_modules/svg.pathmorphing.js": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
|
||||
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
|
||||
"dependencies": {
|
||||
"svg.js": "^2.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svg.resize.js": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
|
||||
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
|
||||
"dependencies": {
|
||||
"svg.js": "^2.6.5",
|
||||
"svg.select.js": "^2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svg.resize.js/node_modules/svg.select.js": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
|
||||
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
|
||||
"dependencies": {
|
||||
"svg.js": "^2.2.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/svg.select.js": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
|
||||
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
|
||||
"dependencies": {
|
||||
"svg.js": "^2.6.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": {
|
||||
"version": "5.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz",
|
||||
"integrity": "sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ=="
|
||||
"version": "5.15.3",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
|
||||
"integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w=="
|
||||
},
|
||||
"a17t": {
|
||||
"version": "0.5.1",
|
||||
@@ -14,9 +155,9 @@
|
||||
"integrity": "sha512-peIPrH9eDiu49LLzLlSTFFrXj6WLlEX3TRsUkqyyOHi/i58ilJ/eERnu7AcswXhuCBx+/2W9EUuHM+8iAq4ipg=="
|
||||
},
|
||||
"apexcharts": {
|
||||
"version": "3.24.0",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.24.0.tgz",
|
||||
"integrity": "sha512-iT6czJCIVrmAtrcO90MZTQCvC+xi6R6Acf0jNH/d40FVTtCfcqECuKIh5iAMyOTtgUb7+fQ8rbadH2bm1kbL9Q==",
|
||||
"version": "3.26.2",
|
||||
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.26.2.tgz",
|
||||
"integrity": "sha512-CD7bad4ygwc9rs9vOQDDagUcoJ1mcc9BwNSiQB14l6jiZBCQKrXxnG4I1ZjJ2MIel/Y5GmsJFs8HTcZBqpe/Ew==",
|
||||
"requires": {
|
||||
"svg.draggable.js": "^2.2.2",
|
||||
"svg.easing.js": "^2.0.0",
|
||||
@@ -26,15 +167,20 @@
|
||||
"svg.select.js": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"flag-icon-css": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/flag-icon-css/-/flag-icon-css-3.5.0.tgz",
|
||||
"integrity": "sha512-pgJnJLrtb0tcDgU1fzGaQXmR8h++nXvILJ+r5SmOXaaL/2pocunQo2a8TAXhjQnBpRLPtZ1KCz/TYpqeNuE2ew=="
|
||||
},
|
||||
"inter-ui": {
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/inter-ui/-/inter-ui-3.15.0.tgz",
|
||||
"integrity": "sha512-6v0WK8FHkVYbNQZ7L9O5tP8280pgTBR9ydxqYwssMuUH6SZO70ZFK/NQ1Ob8nNmOOzpUJAzT0WE73ty96z1tAQ=="
|
||||
"version": "3.18.1",
|
||||
"resolved": "https://registry.npmjs.org/inter-ui/-/inter-ui-3.18.1.tgz",
|
||||
"integrity": "sha512-W3LnAirp6a1ixpAHZwr9gH52KlOQOAp0oqbmIoGi2dAIlcIB7auJgLr9XFHUzYy2FoZ0Nf7aPe/nHMZB4/Zvdg=="
|
||||
},
|
||||
"litepicker": {
|
||||
"version": "1.5.7",
|
||||
"resolved": "https://registry.npmjs.org/litepicker/-/litepicker-1.5.7.tgz",
|
||||
"integrity": "sha512-4L2ZcF8iqCE4A/qGWS3PbdFplZR1g751x5SsZ87zCRZ4LQN1Fgezarnvqi0eHk/kDWK7Qx0HZ9Y4bNznJMF1xA=="
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/litepicker/-/litepicker-2.0.11.tgz",
|
||||
"integrity": "sha512-7MECMp2EDGIYDIz9QT24t9hWpgBD9JD57ZdDrbffNMGfbw0JVhBhvlYsyaIUuYhywtLvgmI5lfulM7XF2HLEkg=="
|
||||
},
|
||||
"svg.draggable.js": {
|
||||
"version": "2.2.2",
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
"a17t": "^0.5.1",
|
||||
"apexcharts": "^3.24.0",
|
||||
"flag-icon-css": "^3.5.0",
|
||||
"inter-ui": "^3.15.0",
|
||||
"litepicker": "^1.5.7"
|
||||
"litepicker": "^2.0.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-28 21:38
|
||||
|
||||
from django.db.models.expressions import F
|
||||
from ..models import Session, Hit
|
||||
from django.db import migrations, models
|
||||
from django.db.models import Subquery, OuterRef
|
||||
|
||||
|
||||
def update_bounce_stats(_a, _b):
|
||||
Session.objects.all().annotate(hit_count=models.Count("hit")).filter(
|
||||
hit_count__gt=1
|
||||
).update(is_bounce=False)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -23,6 +14,5 @@ class Migration(migrations.Migration):
|
||||
model_name="session",
|
||||
name="is_bounce",
|
||||
field=models.BooleanField(db_index=True, default=True),
|
||||
),
|
||||
migrations.RunPython(update_bounce_stats, lambda: ()),
|
||||
)
|
||||
]
|
||||
|
||||
21
shynet/analytics/migrations/0009_auto_20210329_1100.py
Normal file
21
shynet/analytics/migrations/0009_auto_20210329_1100.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-29 15:00
|
||||
|
||||
from django.db import migrations, models
|
||||
from ..models import Session
|
||||
|
||||
|
||||
def update_bounce_stats(_a, _b):
|
||||
Session.objects.all().annotate(hit_count=models.Count("hit")).filter(
|
||||
hit_count__gt=1
|
||||
).update(is_bounce=False)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("analytics", "0008_session_is_bounce"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_bounce_stats, lambda: ()),
|
||||
]
|
||||
@@ -1,12 +1,10 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.shortcuts import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import Service
|
||||
from core.models import Service, ACTIVE_USER_TIMEDELTA
|
||||
|
||||
|
||||
def _default_uuid():
|
||||
@@ -61,9 +59,7 @@ class Session(models.Model):
|
||||
|
||||
@property
|
||||
def is_currently_active(self):
|
||||
return timezone.now() - self.last_seen < timezone.timedelta(
|
||||
milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2
|
||||
)
|
||||
return timezone.now() - self.last_seen < ACTIVE_USER_TIMEDELTA
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
|
||||
@@ -39,6 +39,9 @@ def _geoip2_lookup(ip):
|
||||
}
|
||||
except geoip2.errors.AddressNotFoundError:
|
||||
return {}
|
||||
except FileNotFoundError as e:
|
||||
log.exception("Unable to perform GeoIP lookup: %s", e)
|
||||
return {}
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -58,6 +61,7 @@ def ingress_request(
|
||||
log.debug(f"Linked to service {service}")
|
||||
|
||||
if dnt and service.respect_dnt:
|
||||
log.debug("Ignoring because of DNT")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -67,6 +71,7 @@ def ingress_request(
|
||||
ignored_network.version == remote_ip.version
|
||||
and ignored_network.supernet_of(remote_ip)
|
||||
):
|
||||
log.debug("Ignoring because of ignored IP")
|
||||
return
|
||||
except ValueError as e:
|
||||
log.exception(e)
|
||||
@@ -197,4 +202,5 @@ def ingress_request(
|
||||
)
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
print(e)
|
||||
raise e
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
var Shynet = {
|
||||
idempotency: null,
|
||||
heartbeatTaskId: null,
|
||||
skipHeartbeat: false,
|
||||
sendHeartbeat: function () {
|
||||
try {
|
||||
if (document.hidden) {
|
||||
if (document.hidden || Shynet.skipHeartbeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
Shynet.skipHeartbeat = true;
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open(
|
||||
"POST",
|
||||
@@ -20,6 +23,12 @@ var Shynet = {
|
||||
true
|
||||
);
|
||||
xhr.setRequestHeader("Content-Type", "application/json");
|
||||
xhr.onload = function () {
|
||||
Shynet.skipHeartbeat = false;
|
||||
};
|
||||
xhr.onerror = function () {
|
||||
Shynet.skipHeartbeat = false;
|
||||
};
|
||||
xhr.send(
|
||||
JSON.stringify({
|
||||
idempotency: Shynet.idempotency,
|
||||
@@ -30,13 +39,14 @@ var Shynet = {
|
||||
window.performance.timing.navigationStart,
|
||||
})
|
||||
);
|
||||
} catch (e) { }
|
||||
} catch (e) {}
|
||||
},
|
||||
newPageLoad: function () {
|
||||
if (Shynet.heartbeatTaskId != null) {
|
||||
clearInterval(Shynet.heartbeatTaskId);
|
||||
}
|
||||
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.sendHeartbeat();
|
||||
}
|
||||
@@ -51,4 +61,4 @@ window.addEventListener("load", Shynet.newPageLoad);
|
||||
// -- START --
|
||||
{{script_inject|safe}}
|
||||
// -- END --
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
@@ -54,7 +54,7 @@ class ValidateServiceOriginsMixin:
|
||||
origins = service.origins
|
||||
cache.set(f"service_origins_{service_uuid}", origins, timeout=3600)
|
||||
|
||||
resp = super().dispatch(request, *args, **kwargs)
|
||||
allow_origin = "*"
|
||||
|
||||
if origins != "*":
|
||||
remote_origin = request.META.get("HTTP_ORIGIN")
|
||||
@@ -66,12 +66,12 @@ class ValidateServiceOriginsMixin:
|
||||
remote_origin = f"{parsed.scheme}://{parsed.netloc}".lower()
|
||||
origins = [origin.strip().lower() for origin in origins.split(",")]
|
||||
if remote_origin in origins:
|
||||
resp["Access-Control-Allow-Origin"] = remote_origin
|
||||
allow_origin = remote_origin
|
||||
else:
|
||||
return HttpResponseForbidden()
|
||||
else:
|
||||
resp["Access-Control-Allow-Origin"] = "*"
|
||||
|
||||
resp = super().dispatch(request, *args, **kwargs)
|
||||
resp["Access-Control-Allow-Origin"] = allow_origin
|
||||
resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST"
|
||||
resp[
|
||||
"Access-Control-Allow-Headers"
|
||||
|
||||
@@ -4,14 +4,20 @@ import re
|
||||
import uuid
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.core.exceptions import ValidationError
|
||||
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.shortcuts import reverse
|
||||
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(
|
||||
milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2
|
||||
)
|
||||
|
||||
|
||||
def _default_uuid():
|
||||
return str(uuid.uuid4())
|
||||
@@ -119,8 +125,10 @@ class Service(models.Model):
|
||||
Session = apps.get_model("analytics", "Session")
|
||||
Hit = apps.get_model("analytics", "Hit")
|
||||
|
||||
tz_now = timezone.now()
|
||||
|
||||
currently_online = Session.objects.filter(
|
||||
service=self, last_seen__gt=timezone.now() - timezone.timedelta(seconds=10)
|
||||
service=self, last_seen__gt=tz_now - ACTIVE_USER_TIMEDELTA
|
||||
).count()
|
||||
|
||||
sessions = Session.objects.filter(
|
||||
@@ -133,6 +141,8 @@ class Service(models.Model):
|
||||
)
|
||||
hit_count = hits.count()
|
||||
|
||||
has_hits = Hit.objects.filter(service=self).exists()
|
||||
|
||||
bounces = sessions.filter(is_bounce=True)
|
||||
bounce_count = bounces.count()
|
||||
|
||||
@@ -202,22 +212,41 @@ class Service(models.Model):
|
||||
if session_count == 0:
|
||||
avg_session_duration = None
|
||||
|
||||
session_chart_data = {
|
||||
k["date"]: k["count"]
|
||||
for k in sessions.annotate(date=TruncDate("start_time"))
|
||||
.values("date")
|
||||
.annotate(count=models.Count("uuid"))
|
||||
.order_by("date")
|
||||
}
|
||||
for day_offset in range((end_time - start_time).days + 1):
|
||||
day = (start_time + timezone.timedelta(days=day_offset)).date()
|
||||
if day not in session_chart_data:
|
||||
session_chart_data[day] = 0
|
||||
# Show hourly chart for date ranges of 3 days or less, otherwise daily chart
|
||||
if (end_time - start_time).days < 3:
|
||||
session_chart_tooltip_format = "MM/dd HH:mm"
|
||||
session_chart_granularity = "hourly"
|
||||
session_chart_data = {
|
||||
k["hour"]: k["count"]
|
||||
for k in sessions.annotate(hour=TruncHour("start_time"))
|
||||
.values("hour")
|
||||
.annotate(count=models.Count("uuid"))
|
||||
.order_by("hour")
|
||||
}
|
||||
for hour_offset in range(int((end_time - start_time).total_seconds() / 3600) + 1):
|
||||
hour = (start_time + timezone.timedelta(hours=hour_offset))
|
||||
if hour not in session_chart_data:
|
||||
session_chart_data[hour] = 0 if hour <= tz_now else None
|
||||
else:
|
||||
session_chart_tooltip_format = "MMM d"
|
||||
session_chart_granularity = "daily"
|
||||
session_chart_data = {
|
||||
k["date"]: k["count"]
|
||||
for k in sessions.annotate(date=TruncDate("start_time"))
|
||||
.values("date")
|
||||
.annotate(count=models.Count("uuid"))
|
||||
.order_by("date")
|
||||
}
|
||||
for day_offset in range((end_time - start_time).days + 1):
|
||||
day = (start_time + timezone.timedelta(days=day_offset)).date()
|
||||
if day not in session_chart_data:
|
||||
session_chart_data[day] = 0 if day <= tz_now.date() else None
|
||||
|
||||
return {
|
||||
"currently_online": currently_online,
|
||||
"session_count": session_count,
|
||||
"hit_count": hit_count,
|
||||
"has_hits": has_hits,
|
||||
"avg_hits_per_session": hit_count / (max(session_count, 1)),
|
||||
"bounce_rate_pct": bounce_count * 100 / session_count
|
||||
if session_count > 0
|
||||
@@ -240,6 +269,8 @@ class Service(models.Model):
|
||||
)
|
||||
]
|
||||
),
|
||||
"session_chart_tooltip_format": session_chart_tooltip_format,
|
||||
"session_chart_granularity": session_chart_granularity,
|
||||
"online": True,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
from datetime import datetime, time
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class DateRangeMixin:
|
||||
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(
|
||||
self.request.GET.get("startDate"), "%Y-%m-%d"
|
||||
)
|
||||
@@ -15,7 +14,7 @@ class DateRangeMixin:
|
||||
return timezone.now() - timezone.timedelta(days=30)
|
||||
|
||||
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(
|
||||
self.request.GET.get("endDate"), "%Y-%m-%d"
|
||||
)
|
||||
@@ -23,8 +22,45 @@ class DateRangeMixin:
|
||||
else:
|
||||
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):
|
||||
data = super().get_context_data(**kwargs)
|
||||
data["start_date"] = self.get_start_date()
|
||||
data["end_date"] = self.get_end_date()
|
||||
data["date_ranges"] = self.get_date_ranges()
|
||||
|
||||
return data
|
||||
|
||||
@@ -15,6 +15,14 @@
|
||||
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 {
|
||||
--color-neutral-000: white;
|
||||
--color-neutral-50: #F8FAFC;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div>
|
||||
<h4 class="heading">{% block page_title %}{% endblock %}</h4>
|
||||
<div class="flex-1 truncate">
|
||||
<h4 class="heading truncate">{% block page_title %}{% endblock %}</h4>
|
||||
</div>
|
||||
<hr class="sep">
|
||||
{% block main %}
|
||||
|
||||
@@ -10,30 +10,34 @@
|
||||
{% include 'a17t/includes/head.html' %}
|
||||
<link rel="icon" type="image/png" href="{% static 'dashboard/images/icon.png' %}">
|
||||
<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>
|
||||
<link rel="stylesheet" href="{% static 'litepicker/dist/css/litepicker.css' %}">
|
||||
|
||||
<script src="{% static 'dashboard/js/base.js' %}"></script>
|
||||
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
|
||||
<link rel="stylesheet" href="{% static 'flag-icon-css/css/flag-icon.min.css' %}">
|
||||
{% block extra_head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body class="bg-neutral-100 min-h-full">
|
||||
<body class="bg-neutral-100 min-h-full overflow-x-hidden">
|
||||
{% block body %}
|
||||
|
||||
<section class="max-w-screen-xl mx-auto px-4 py-4 md:py-12 md:flex">
|
||||
<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' %}">
|
||||
<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>
|
||||
</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')">
|
||||
<span class="icon">
|
||||
<i class="fas fa-bars"></i>
|
||||
</span>
|
||||
</button>
|
||||
</a>
|
||||
<hr class="sep h-4 md:h-8 w-full">
|
||||
<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">
|
||||
@@ -41,8 +45,8 @@
|
||||
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Services</p>
|
||||
|
||||
{% for service in user.owning_services.all %}
|
||||
{% 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 %}
|
||||
{% contextual_url 'dashboard:service' service.uuid as url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name url=url icon=service.link|iconify %}
|
||||
{% endfor %}
|
||||
|
||||
{% endif %}
|
||||
@@ -60,8 +64,8 @@
|
||||
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Collaborations</p>
|
||||
|
||||
{% for service in user.collaborating_services.all %}
|
||||
{% url 'dashboard:service' service.uuid as url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name|truncatechars:20 url=url %}
|
||||
{% contextual_url 'dashboard:service' service.uuid as url %}
|
||||
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name url=url %}
|
||||
{% endfor %}
|
||||
|
||||
<hr class="sep h-8">
|
||||
@@ -121,4 +125,4 @@
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -2,32 +2,48 @@
|
||||
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
|
||||
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
|
||||
</form>
|
||||
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral bg-neutral-000 cursor-pointer" readonly>
|
||||
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral bg-neutral-000 cursor-pointer w-auto" readonly>
|
||||
<style>
|
||||
:root {
|
||||
--litepickerMonthButtonHover: var(--color-urge);
|
||||
--litepickerDayColorHover: var(--color-urge);
|
||||
--litepickerDayIsTodayColor: var(--color-urge);
|
||||
--litepickerDayIsInRange: var(--color-urge-normal-fill);
|
||||
--litepickerDayIsStartBg: var(--color-urge);
|
||||
--litepickerDayIsEndBg: var(--color-urge);
|
||||
--litepickerButtonApplyBg: var(--color-urge);
|
||||
--litepicker-button-prev-month-color-hover: var(--color-urge);
|
||||
--litepicker-button-next-month-color-hover: var(--color-urge);
|
||||
--litepicker-day-color-hover: var(--color-urge);
|
||||
--litepicker-is-today-color: var(--color-urge);
|
||||
--litepicker-is-in-range-color: var(--color-urge-normal-fill);
|
||||
--litepicker-is-start-color-bg: 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>
|
||||
<script>
|
||||
var picker = new Litepicker({
|
||||
element: document.getElementById('rangePicker'),
|
||||
plugins: ['ranges'],
|
||||
singleMode: false,
|
||||
format: 'MMM D, YYYY',
|
||||
format: "MMM DD 'YY",
|
||||
maxDate: new Date(),
|
||||
startDate: Date.parse(document.getElementById("startDate").getAttribute("value")),
|
||||
endDate: Date.parse(document.getElementById("endDate").getAttribute("value")),
|
||||
onSelect: function (startDate, endDate) {
|
||||
document.getElementById("startDate").setAttribute("value", startDate.getFullYear() +
|
||||
"-" + (startDate.getMonth() + 1) + "-" + startDate.getDate());
|
||||
document.getElementById("endDate").setAttribute("value", endDate.getFullYear() + "-" +
|
||||
(endDate.getMonth() + 1) + "-" + endDate.getDate());
|
||||
document.getElementById("datePicker").submit();
|
||||
ranges: {
|
||||
customRanges: {
|
||||
{% for date_range in date_ranges %}
|
||||
'{{ date_range.name }}': [
|
||||
new Date('{{ date_range.start.isoformat }}'),
|
||||
new Date('{{ date_range.end.isoformat }}')
|
||||
],
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
picker.on('selected', (startDate, endDate) => {
|
||||
document.getElementById("startDate").setAttribute("value", startDate.getFullYear() +
|
||||
"-" + (startDate.getMonth() + 1) + "-" + startDate.getDate());
|
||||
document.getElementById("endDate").setAttribute("value", endDate.getFullYear() + "-" +
|
||||
(endDate.getMonth() + 1) + "-" + endDate.getDate());
|
||||
document.getElementById("datePicker").submit();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{% load humanize helpers %}
|
||||
|
||||
<a class="card ~neutral !low service mb-6 p-0" href="{% 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 %}
|
||||
<div class="p-4 md:flex justify-between">
|
||||
<div class="flex items-center mb-4 md:mb-0">
|
||||
<h3 class="heading text-xl md:text-2xl mr-2 mb-1 text-urge-600 flex items-center">
|
||||
<div class="p-4 md:flex justify-between overflow-none">
|
||||
<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 truncate" title="{{object.name}}">
|
||||
{{object.link|iconify}}
|
||||
<span>{{object.name}}</span>
|
||||
<span class="truncate">{{object.name}}</span>
|
||||
</h3>
|
||||
{% include 'dashboard/includes/stats_status_chip.html' %}
|
||||
</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>
|
||||
<p>Sessions</p>
|
||||
<p class="label">
|
||||
@@ -51,7 +51,7 @@
|
||||
</div>
|
||||
<hr class="sep h-4">
|
||||
<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.session_chart_data sparkline=True height=100 name=object.uuid tooltip_format=stats.session_chart_tooltip_format granularity=stats.session_chart_granularity %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
</a>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div class="card ~neutral !high font-mono text-sm whitespace-pre-wrap break-all">{% filter force_escape %}<noscript>
|
||||
<img src="{{script_protocol}}{{request.site.domain}}{% url 'ingress:endpoint_pixel' object.uuid %}">
|
||||
</noscript>
|
||||
<script defer src="{{script_protocol}}{{request.site.domain}}{% url 'ingress:endpoint_script' object.uuid %}"></script>{% endfilter %}
|
||||
</div>
|
||||
@@ -12,7 +12,7 @@
|
||||
{% for session in object_list %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'dashboard:service_session' object.pk session.pk %}"
|
||||
<a href="{% contextual_url 'dashboard:service_session' object.pk session.pk %}"
|
||||
class="font-medium text-urge-700">
|
||||
{{session.start_time|date:"M j Y, g:i a"|capfirst}}
|
||||
{% if session.is_currently_active %}
|
||||
@@ -27,7 +27,7 @@
|
||||
<span class="text-gray-600">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td>
|
||||
<td><span class="{{session.country|flag_class}}"></span>{{session.asn|default:"Unknown"}}</td>
|
||||
<td class="rf">{{session.duration|naturaldelta}}</td>
|
||||
<td class="rf">{{session.hit_set.count|intcomma}}</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{% load helpers %}
|
||||
|
||||
<div>
|
||||
<a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-neutral-100{% endif %} flex items-center"
|
||||
{% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{icon}} {{label}}</a>
|
||||
<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}} <span class="truncate">{{label}}</span></a>
|
||||
</div>
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
{% with stats=object.get_daily_stats %}
|
||||
{% if stats.currently_online > 0 %}
|
||||
<span class="chip ~positive !high">
|
||||
<span class="chip ~positive !high whitespace-nowrap">
|
||||
{{stats.currently_online|intcomma}} online
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
},
|
||||
tooltip: {
|
||||
shared: false,
|
||||
x: {
|
||||
format: '{{tooltip_format|default:"MMM d"}}',
|
||||
},
|
||||
},
|
||||
colors: ["#805AD5"],
|
||||
chart: {
|
||||
@@ -34,6 +37,14 @@
|
||||
stops: [0, 75, 100]
|
||||
},
|
||||
},
|
||||
{% if granularity == "daily" and click_zoom %}
|
||||
events: {
|
||||
markerClick: function(event, chartContext, { seriesIndex, dataPointIndex, w: {config}}) {
|
||||
const day = config.series[seriesIndex].data[dataPointIndex].x
|
||||
window.location.href = `?startDate=${day}&endDate=${day}`
|
||||
},
|
||||
},
|
||||
{% endif %}
|
||||
},
|
||||
grid: {
|
||||
padding: {
|
||||
@@ -63,6 +74,9 @@
|
||||
},
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
labels: {
|
||||
datetimeUTC: false
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
width: 1.5,
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="md:flex justify-between items-center">
|
||||
<div>
|
||||
<h4 class="heading">{{request.site.name|default:"Dashboard"}}</h4>
|
||||
<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 truncate">{{request.site.name|default:"Dashboard"}}</h4>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<div class="mr-1">
|
||||
@@ -17,13 +17,15 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="sep">
|
||||
<hr class="sep h-8 md:h-12">
|
||||
{% for object in object_list|dictsortreversed:"stats.session_count" %}
|
||||
{% include 'dashboard/includes/service_overview.html' %}
|
||||
{% empty %}
|
||||
<p>You don't have any services on {{request.site.name|default:"Shynet"}} yet.</p>
|
||||
<p class="aside ~urge !high">You don't have any services yet. {% if can_create %}Get started by <a href="{% url 'dashboard:service_create' %}" class="underline">creating one</a>.{% endif %}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% if object_list %}
|
||||
{% pagination page_obj request %}
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -6,11 +6,19 @@
|
||||
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
|
||||
{% has_perm 'core.change_service' user object as can_update %}
|
||||
{% if can_update %}
|
||||
<a href="{% url 'dashboard:service_update' service.uuid %}" class="button field bg-neutral-000 w-auto">Manage →</a>
|
||||
<a href="{% contextual_url 'dashboard:service_update' service.uuid %}" class="button field bg-neutral-000 w-auto">Manage →</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
{% if not stats.has_hits %}
|
||||
<div class="content mb-6">
|
||||
<p>
|
||||
This service hasn't collected any data yet. To get started, place the following code snippet at the end of the <code><body></code> tag on any page you'd like to track.
|
||||
</p>
|
||||
{% include 'dashboard/includes/service_snippet.html' %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="grid grid-cols-2 gap-6 md:flex justify-between mb-6 card ~neutral !high px-6" id="stats">
|
||||
{% with classes="text-sm font-semibold" good_classes="text-positive-400" bad_classes="text-critical-400" neutral_classes="text-gray-400" %}
|
||||
<article class="">
|
||||
@@ -85,9 +93,10 @@
|
||||
</article>
|
||||
{% endwith %}
|
||||
</div>
|
||||
<div class="card ~neutral !low py-0 mb-6">
|
||||
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data %}
|
||||
<div class="card overflow-visible ~neutral !low py-0 mb-6">
|
||||
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data tooltip_format=stats.session_chart_tooltip_format granularity=stats.session_chart_granularity click_zoom=True %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
<table class="table">
|
||||
@@ -100,7 +109,7 @@
|
||||
<tbody>
|
||||
{% for location in stats.locations %}
|
||||
<tr>
|
||||
<td>{{location.location|default:"Unknown"|urldisplay}}</td>
|
||||
<td class="truncate w-full max-w-0">{{location.location|default:"Unknown"|urldisplay}}</td>
|
||||
<td class="rf">{{location.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
@@ -122,7 +131,7 @@
|
||||
<tbody>
|
||||
{% for referrer in stats.referrers %}
|
||||
<tr>
|
||||
<td>{{referrer.referrer|default:"Direct"|urldisplay}}</td>
|
||||
<td class="truncate w-full max-w-0">{{referrer.referrer|default:"Direct"|urldisplay}}</td>
|
||||
<td class="rf">{{referrer.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
@@ -144,7 +153,9 @@
|
||||
<tbody>
|
||||
{% for country in stats.countries %}
|
||||
<tr>
|
||||
<td>{{country.country|flag_emoji}} {{country.country|country_name}}</td>
|
||||
<td class="truncate w-full max-w-0" title="{{country.country|country_name}}">
|
||||
<span class="{{country.country|flag_class}}"></span> {{country.country|country_name}}
|
||||
</td>
|
||||
<td class="rf">{{country.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
@@ -166,7 +177,9 @@
|
||||
<tbody>
|
||||
{% for os in stats.operating_systems %}
|
||||
<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" title="{{os.os|default:'Unknown'}}">
|
||||
{{os.os|iconify}}<span>{{os.os|default:"Unknown"}}</span>
|
||||
</td>
|
||||
<td class="rf">{{os.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
@@ -188,7 +201,7 @@
|
||||
<tbody>
|
||||
{% for browser in stats.browsers %}
|
||||
<tr>
|
||||
<td class="flex items-center">
|
||||
<td class="flex items-center truncate w-full max-w-0" title="{{browser.browser|default:'Unknown'}}">
|
||||
{{browser.browser|iconify}}<span>{{browser.browser|default:"Unknown"}}</span></td>
|
||||
<td class="rf">{{browser.count|intcomma}}</td>
|
||||
</tr>
|
||||
@@ -211,7 +224,7 @@
|
||||
<tbody>
|
||||
{% for device_type in stats.device_types %}
|
||||
<tr>
|
||||
<td>{{device_type.device_type|default:"Unknown"|title}}</td>
|
||||
<td class="truncate w-full max-w-0">{{device_type.device_type|default:"Unknown"|title}}</td>
|
||||
<td class="rf">{{device_type.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
@@ -225,8 +238,8 @@
|
||||
</div>
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
{% include 'dashboard/includes/session_list.html' %}
|
||||
<hr class="sep h-8">
|
||||
<a href="{% url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more
|
||||
<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
|
||||
sessions
|
||||
→</a>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{% block head_title %}{{object.name}} Session{% endblock %}
|
||||
|
||||
{% block service_actions %}
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">Analytics →</a>
|
||||
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">Analytics →</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block service_content %}
|
||||
@@ -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>
|
||||
</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>
|
||||
<p>Browser</p>
|
||||
@@ -45,13 +45,13 @@
|
||||
</div>
|
||||
<div>
|
||||
<p>Country</p>
|
||||
<p class="label">{{session.country|flag_emoji}} {{session.country|country_name}}</p>
|
||||
<p class="label"><span class="{{session.country|flag_class}}"></span>{{session.country|country_name}}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>Location</p>
|
||||
<p class="label">
|
||||
{% 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>
|
||||
{% else %}
|
||||
Unknown
|
||||
@@ -72,7 +72,7 @@
|
||||
</div>
|
||||
<div class="md:flex card ~neutral !low flex-grow justify-between">
|
||||
<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 %}
|
||||
<p>via {{hit.referrer|urlize}}<p>
|
||||
{% endif %}
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
|
||||
{% block service_actions %}
|
||||
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
|
||||
<a href="{% url 'dashboard:service' object.uuid %}" class="button field ~neutral bg-neutral-000 w-auto">Analytics →</a>
|
||||
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field ~neutral bg-neutral-000 w-auto">Analytics →</a>
|
||||
{% endblock %}
|
||||
|
||||
{% 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' %}
|
||||
</div>
|
||||
{% pagination page_obj request %}
|
||||
|
||||
@@ -12,12 +12,7 @@
|
||||
<div class="max-w-xl content">
|
||||
<h5>Installation</h5>
|
||||
<p>Place the following snippet at the end of the <code><body></code> tag on any page you'd like to track.</p>
|
||||
<div class="card ~neutral !high font-mono text-sm">
|
||||
{% filter force_escape %}<noscript><img
|
||||
src="{{script_protocol}}{{request.site.domain}}{% url 'ingress:endpoint_pixel' object.uuid %}"></noscript>
|
||||
<script defer src="{{script_protocol}}{{request.site.domain}}{% url 'ingress:endpoint_script' object.uuid %}"></script>
|
||||
{% endfilter %}
|
||||
</div>
|
||||
{% include 'dashboard/includes/service_snippet.html' %}
|
||||
<hr class="sep h-4">
|
||||
<h5>Settings</h5>
|
||||
<form class="card ~neutral !low p-0" method="POST">
|
||||
|
||||
@@ -6,21 +6,21 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="md:flex justify-between items-center" id="heading">
|
||||
<a class="flex items-center mb-4 md:mb-0" href="{% url 'dashboard:service' object.uuid %}">
|
||||
<h3 class="heading leading-none mr-4">
|
||||
<a class="flex items-center mb-4 md:mb-0 truncate" href="{% contextual_url 'dashboard:service' object.uuid %}">
|
||||
<h3 class="heading items-center mr-4 md:mr-2 flex truncate">
|
||||
{{object.link|iconify}}
|
||||
{{object.name}}
|
||||
<span class="flex-1 truncate ml-2" title="{{object.name}}">{{object.name}}</span>
|
||||
</h3>
|
||||
<div class='text-3xl'>
|
||||
<div class="text-3xl md:mr-2">
|
||||
{% include 'dashboard/includes/stats_status_chip.html' %}
|
||||
</div>
|
||||
</a>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center flex-none">
|
||||
{% block service_actions %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="sep h-8">
|
||||
<hr class="sep h-8 md:h-12">
|
||||
{% block service_content %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@@ -1,13 +1,14 @@
|
||||
from datetime import timedelta
|
||||
from urllib.parse import urlparse
|
||||
import urllib
|
||||
|
||||
import flag
|
||||
import pycountry
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import SafeString
|
||||
from django.template.defaulttags import url as url_tag
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -27,11 +28,11 @@ def naturaldelta(timedelta):
|
||||
|
||||
|
||||
@register.filter
|
||||
def flag_emoji(isocode):
|
||||
try:
|
||||
return flag.flag(isocode)
|
||||
except:
|
||||
return ""
|
||||
def flag_class(isocode):
|
||||
if isocode:
|
||||
return "mr-1 flag-icon flag-icon-" + isocode.lower()
|
||||
else:
|
||||
return "hidden"
|
||||
|
||||
|
||||
@register.filter
|
||||
@@ -171,7 +172,7 @@ def iconify(text):
|
||||
domain = text + ".com"
|
||||
|
||||
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>'
|
||||
)
|
||||
|
||||
|
||||
@@ -180,7 +181,50 @@ def urldisplay(url):
|
||||
if url.startswith("http"):
|
||||
display_url = url.replace("http://", "").replace("https://", "")
|
||||
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:
|
||||
return url
|
||||
|
||||
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."""
|
||||
|
||||
CONTEXT_PARAMS = ["startDate", "endDate"]
|
||||
|
||||
def __init__(self, urlnode):
|
||||
self.urlnode = urlnode
|
||||
|
||||
def __repr__(self):
|
||||
return self.urlnode.__repr__()
|
||||
|
||||
def render(self, context):
|
||||
url = self.urlnode.render(context)
|
||||
if self.urlnode.asvar:
|
||||
url = context[self.urlnode.asvar]
|
||||
|
||||
url_parts = list(urlparse(url))
|
||||
query = dict(urllib.parse.parse_qsl(url_parts[4]))
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
url_parts[4] = urllib.parse.urlencode(query)
|
||||
|
||||
url_final = urllib.parse.urlunparse(url_parts)
|
||||
|
||||
if self.urlnode.asvar:
|
||||
context[self.urlnode.asvar] = url_final
|
||||
return ''
|
||||
else:
|
||||
return url_final
|
||||
|
||||
|
||||
@register.tag
|
||||
def contextual_url(*args, **kwargs):
|
||||
urlnode = url_tag(*args, **kwargs)
|
||||
return ContextualURLNode(urlnode)
|
||||
|
||||
@register.filter
|
||||
def location_url(session):
|
||||
return settings.LOCATION_URL.replace("$LATITUDE", str(session.latitude)).replace("$LONGITUDE", str(session.longitude))
|
||||
|
||||
@@ -25,7 +25,7 @@ from .mixins import DateRangeMixin
|
||||
class DashboardView(LoginRequiredMixin, DateRangeMixin, ListView):
|
||||
model = Service
|
||||
template_name = "dashboard/pages/dashboard.html"
|
||||
paginate_by = 5
|
||||
paginate_by = settings.DASHBOARD_PAGE_SIZE
|
||||
|
||||
def get_queryset(self):
|
||||
return Service.objects.filter(
|
||||
@@ -66,6 +66,7 @@ class ServiceView(
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
data["script_protocol"] = "https://" if settings.SCRIPT_USE_HTTPS else "http://"
|
||||
data["stats"] = self.object.get_core_stats(data["start_date"], data["end_date"])
|
||||
data["object_list"] = Session.objects.filter(
|
||||
service=self.get_object(),
|
||||
|
||||
@@ -18,7 +18,7 @@ import urllib.parse as urlparse
|
||||
from django.contrib.messages import constants as messages
|
||||
|
||||
# Increment on new releases
|
||||
VERSION = "v0.8.1"
|
||||
VERSION = "v0.9.0"
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
@@ -301,11 +301,16 @@ NPM_ROOT_PATH = "../"
|
||||
NPM_FILE_PATTERNS = {
|
||||
"a17t": [os.path.join("dist", "a17t.css"), os.path.join("dist", "tailwind.css")],
|
||||
"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")],
|
||||
"stimulus": [os.path.join("dist", "stimulus.umd.js")],
|
||||
"inter-ui": [os.path.join("Inter (web)", "*")],
|
||||
"@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", "*")],
|
||||
}
|
||||
|
||||
# Shynet
|
||||
@@ -339,3 +344,9 @@ BLOCK_ALL_IPS = os.getenv("BLOCK_ALL_IPS", "False") == "True"
|
||||
|
||||
# Include date and service ID in salt?
|
||||
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"))
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>Pixel test</title>
|
||||
<title>JS test</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script src="http://localhost:8000/ingress/9b2c4e2f-8d29-4418-82d4-b68e06795025/script.js"></script>
|
||||
<noscript>
|
||||
<img src="http://localhost:8000/ingress/0ca733e8-c41f-462b-a11a-4ba0cea29948/pixel.gif">
|
||||
</noscript>
|
||||
<script defer src="http://localhost:8000/ingress/0ca733e8-c41f-462b-a11a-4ba0cea29948/script.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user