Compare commits
151 Commits
| 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 | ||
|
|
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 | ||
|
|
fe8e766670 | ||
|
|
b63863e283 | ||
|
|
516f9fb951 | ||
|
|
c2234ec647 | ||
|
|
02cbee5c8c | ||
|
|
518436ffd2 | ||
|
|
311aa2b1ac | ||
|
|
8ad44ddc23 | ||
|
|
874aad87a8 | ||
|
|
f2e875d03d | ||
|
|
45fd32c8ca | ||
|
|
08b36ba69f | ||
|
|
d5cfe577a0 | ||
|
|
c131cfef27 | ||
|
|
526d4cd133 | ||
|
|
8e09871b44 | ||
|
|
6aa3ce0b32 | ||
|
|
23ea8e493e | ||
|
|
22d996bed7 | ||
|
|
9df864787c | ||
|
|
b7a6ac9ec0 | ||
|
|
38d8d416e1 | ||
|
|
592613a99a | ||
|
|
e9f43c6a53 | ||
|
|
89c6800913 | ||
|
|
db9c807289 | ||
|
|
6e48a3eac7 | ||
|
|
ba9a716913 | ||
|
|
6d7292a60a | ||
|
|
c0d02732e7 | ||
|
|
d071a91917 | ||
|
|
d67e14b08f | ||
|
|
174a386f54 | ||
|
|
ce23cfc5b5 | ||
|
|
8be690c417 | ||
|
|
2f778dc4b4 | ||
|
|
e0c165313b | ||
|
|
c86192d301 | ||
|
|
775c105d1d | ||
|
|
be85c0a560 | ||
|
|
70e1af15cc | ||
|
|
6afea91c5f | ||
|
|
7a4c892804 | ||
|
|
9b50b1ea42 | ||
|
|
52a18d21f1 | ||
|
|
8aaf312c67 | ||
|
|
ede06900e5 | ||
|
|
a42455c9dc | ||
|
|
547a84f2fc | ||
|
|
f56ea99dc2 | ||
|
|
4cea5d2310 | ||
|
|
e4f09b4e68 | ||
|
|
cc094fe04e | ||
|
|
ac5c743390 | ||
|
|
963db18642 | ||
|
|
748fb76eaf | ||
|
|
d93a698e87 | ||
|
|
ca9ee2f1f5 | ||
|
|
9146c889ac | ||
|
|
8b1034ebb0 |
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 }}
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -138,4 +138,7 @@ dmypy.json
|
||||
secrets.yml
|
||||
.vscode
|
||||
.DS_Store
|
||||
compiledstatic/
|
||||
compiledstatic/
|
||||
|
||||
# Pycharm
|
||||
.idea
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM python:3-alpine
|
||||
FROM python:alpine3.12
|
||||
|
||||
# Getting things ready
|
||||
WORKDIR /usr/src/shynet
|
||||
|
||||
14
Pipfile
14
Pipfile
@@ -3,29 +3,23 @@ name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[pipenv]
|
||||
allow_prereleases = true
|
||||
|
||||
[packages]
|
||||
django = "~=3.0"
|
||||
django = "~=3.1"
|
||||
django-allauth = "~=0.42.0"
|
||||
geoip2 = "~=3.0.0"
|
||||
whitenoise = "~=5.1.0"
|
||||
celery = "~=4.4.6"
|
||||
django-ipware = "~=2.1.0"
|
||||
pyyaml = "~=5.3.1"
|
||||
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"
|
||||
redis = "~=3.5.3"
|
||||
django-redis-cache = "~=2.1.1"
|
||||
django-redis-cache = "~=3.0.0"
|
||||
pycountry = "~=19.8.18"
|
||||
html2text = "~=2020.1.16"
|
||||
django-health-check = "~=3.12.1"
|
||||
django-npm = "~=1.0.0"
|
||||
|
||||
[dev-packages]
|
||||
black = "*"
|
||||
django-debug-toolbar = "*"
|
||||
|
||||
264
Pipfile.lock
generated
264
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "c18d6dc7c78d5f0634e38bb81bc1cf2cd4a0c128d70ca667fe765a66b294e66e"
|
||||
"sha256": "c51ea0205c9ffe753b9ef5249cd49c2338bb50768ae104113bfb7b97b5f9d70c"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {},
|
||||
@@ -19,21 +19,23 @@
|
||||
"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:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a",
|
||||
"sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed"
|
||||
"sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
|
||||
"sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
|
||||
],
|
||||
"version": "==3.2.10"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"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": [
|
||||
@@ -45,32 +47,34 @@
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
|
||||
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
|
||||
"sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
|
||||
"sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
|
||||
],
|
||||
"version": "==2020.6.20"
|
||||
"version": "==2021.5.30"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
"sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
|
||||
"sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
"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": {
|
||||
"hashes": [
|
||||
"sha256:8ede8ba04cf5bf7999e1492fa77df545db83717f52c5eab625f97228ebd539bf",
|
||||
"sha256:aa621655d72cdd30f57073893b96cd0c3831a85b08b8e4954531bdac47e3e8c8"
|
||||
"sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
|
||||
"sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"
|
||||
],
|
||||
"version": "==0.7.0rc1"
|
||||
"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:a2127ad0150ec6966655bedf15dbbff9697cc86d61653db2da1afa506c0b04cc",
|
||||
"sha256:c93c28ccf1d094cbd00d860e83128a39e45d2c571d3b54361713aaaf9a94cac4"
|
||||
"sha256:a523d62b7ab2908f551dabc32b99017a86aa7784e32b761708e52be3dce6d35d",
|
||||
"sha256:dc41bf07357f1f4810c1c555b685cb51f780b41e37892d6cc92b89789f2847e1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.1.2"
|
||||
"version": "==3.1.12"
|
||||
},
|
||||
"django-allauth": {
|
||||
"hashes": [
|
||||
@@ -79,6 +83,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==0.42.0"
|
||||
},
|
||||
"django-debug-toolbar": {
|
||||
"hashes": [
|
||||
"sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33",
|
||||
"sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.2.1"
|
||||
},
|
||||
"django-health-check": {
|
||||
"hashes": [
|
||||
"sha256:2667b89b8f85ad9b2a24c90581b376016d22ea912fedf37f9866413a3c2e0a5d",
|
||||
@@ -103,19 +115,10 @@
|
||||
},
|
||||
"django-redis-cache": {
|
||||
"hashes": [
|
||||
"sha256:9b2c45a1bc0f295bccd56c2542d937665ae98f3325f20b3d82fc620e14395d52",
|
||||
"sha256:e72691539be99c0b2dd64ac380e26f4d2be5c53c1b2d26845dd279ae38b47477"
|
||||
"sha256:9a2eebef421d996a82098a19d17ff6b321265cd73178fa398913019764e8394a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.1.3"
|
||||
},
|
||||
"emoji-country-flag": {
|
||||
"hashes": [
|
||||
"sha256:67c0cb6a3765fb53f31b34160d6b1c8a5f44b297bc278d1835c6f2e5b0a9a592",
|
||||
"sha256:ae7edb38077b0840210fa9e37673f481f2b9c032446e13ad6dab2b1108cd7ad6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.2.1"
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"geoip2": {
|
||||
"hashes": [
|
||||
@@ -146,6 +149,7 @@
|
||||
"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": {
|
||||
@@ -153,29 +157,34 @@
|
||||
"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:b95d8ed21799e6604683669c7ed3c6a184fcd92434d5762dccdb139b4f29e597"
|
||||
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
|
||||
],
|
||||
"version": "==2.0.2"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.0.3"
|
||||
},
|
||||
"oauthlib": {
|
||||
"hashes": [
|
||||
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
|
||||
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
|
||||
"sha256:42bf6354c2ed8c6acb54d971fce6f88193d97297e18602a3a886603f9d7730cc",
|
||||
"sha256:8f0215fcc533dd8dd1bee6f4c412d4f0cd7297307d43ac61666389e3bc3198a3"
|
||||
],
|
||||
"version": "==3.1.0"
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.1.1"
|
||||
},
|
||||
"psycopg2-binary": {
|
||||
"hashes": [
|
||||
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
|
||||
"sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
|
||||
"sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
|
||||
"sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6",
|
||||
"sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
|
||||
"sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
|
||||
"sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
|
||||
"sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056",
|
||||
"sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
|
||||
"sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
|
||||
"sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
|
||||
@@ -223,27 +232,45 @@
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed",
|
||||
"sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048"
|
||||
"sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
|
||||
"sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
|
||||
],
|
||||
"version": "==2020.1"
|
||||
"version": "==2021.1"
|
||||
},
|
||||
"pyyaml": {
|
||||
"hashes": [
|
||||
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
|
||||
"sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
|
||||
"sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
|
||||
"sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
|
||||
"sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
|
||||
"sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
|
||||
"sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
|
||||
"sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
|
||||
"sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
|
||||
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
|
||||
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
|
||||
"sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
|
||||
"sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
|
||||
"sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
|
||||
"sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
|
||||
"sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
|
||||
"sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
|
||||
"sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
|
||||
"sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
|
||||
"sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
|
||||
"sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
|
||||
"sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e",
|
||||
"sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253",
|
||||
"sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347",
|
||||
"sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183",
|
||||
"sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541",
|
||||
"sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb",
|
||||
"sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185",
|
||||
"sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc",
|
||||
"sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db",
|
||||
"sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa",
|
||||
"sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46",
|
||||
"sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122",
|
||||
"sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b",
|
||||
"sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63",
|
||||
"sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df",
|
||||
"sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc",
|
||||
"sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247",
|
||||
"sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6",
|
||||
"sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==5.3.1"
|
||||
"version": "==5.4.1"
|
||||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
@@ -255,15 +282,17 @@
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
|
||||
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
|
||||
"sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
|
||||
"sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
|
||||
],
|
||||
"version": "==2.24.0"
|
||||
"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:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a",
|
||||
"sha256:fa6c47b933f01060936d87ae9327fead68768b69c6c9ea2109c48be30f2d4dbc"
|
||||
],
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
@@ -274,18 +303,12 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.2"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
|
||||
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
|
||||
],
|
||||
"version": "==1.15.0"
|
||||
},
|
||||
"sqlparse": {
|
||||
"hashes": [
|
||||
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
|
||||
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.4.1"
|
||||
},
|
||||
"ua-parser": {
|
||||
@@ -298,10 +321,11 @@
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a",
|
||||
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461"
|
||||
"sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
|
||||
"sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
|
||||
],
|
||||
"version": "==1.25.10"
|
||||
"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": {
|
||||
"hashes": [
|
||||
@@ -316,6 +340,7 @@
|
||||
"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": {
|
||||
@@ -327,114 +352,5 @@
|
||||
"version": "==5.1.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"appdirs": {
|
||||
"hashes": [
|
||||
"sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
|
||||
"sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
|
||||
],
|
||||
"version": "==1.4.4"
|
||||
},
|
||||
"black": {
|
||||
"hashes": [
|
||||
"sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.8b1"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
|
||||
"sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
|
||||
],
|
||||
"version": "==7.1.2"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
|
||||
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
|
||||
],
|
||||
"version": "==0.4.3"
|
||||
},
|
||||
"pathspec": {
|
||||
"hashes": [
|
||||
"sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
|
||||
"sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"
|
||||
],
|
||||
"version": "==0.8.0"
|
||||
},
|
||||
"regex": {
|
||||
"hashes": [
|
||||
"sha256:1a16afbfadaadc1397353f9b32e19a65dc1d1804c80ad73a14f435348ca017ad",
|
||||
"sha256:2308491b3e6c530a3bb38a8a4bb1dc5fd32cbf1e11ca623f2172ba17a81acef1",
|
||||
"sha256:39a5ef30bca911f5a8a3d4476f5713ed4d66e313d9fb6755b32bec8a2e519635",
|
||||
"sha256:3d5a8d007116021cf65355ada47bf405656c4b3b9a988493d26688275fde1f1c",
|
||||
"sha256:4302153abb96859beb2c778cc4662607a34175065fc2f33a21f49eb3fbd1ccd3",
|
||||
"sha256:463e770c48da76a8da82b8d4a48a541f314e0df91cbb6d873a341dbe578efafd",
|
||||
"sha256:46ab6070b0d2cb85700b8863b3f5504c7f75d8af44289e9562195fe02a8dd72d",
|
||||
"sha256:4f5c0fe46fb79a7adf766b365cae56cafbf352c27358fda811e4a1dc8216d0db",
|
||||
"sha256:60c4f64d9a326fe48e8738c3dbc068e1edc41ff7895a9e3723840deec4bc1c28",
|
||||
"sha256:671c51d352cfb146e48baee82b1ee8d6ffe357c292f5e13300cdc5c00867ebfc",
|
||||
"sha256:6cf527ec2f3565248408b61dd36e380d799c2a1047eab04e13a2b0c15dd9c767",
|
||||
"sha256:7c4fc5a8ec91a2254bb459db27dbd9e16bba1dabff638f425d736888d34aaefa",
|
||||
"sha256:850339226aa4fec04916386577674bb9d69abe0048f5d1a99f91b0004bfdcc01",
|
||||
"sha256:8ba3efdd60bfee1aa784dbcea175eb442d059b576934c9d099e381e5a9f48930",
|
||||
"sha256:8c8c42aa5d3ac9a49829c4b28a81bebfa0378996f9e0ca5b5ab8a36870c3e5ee",
|
||||
"sha256:8e7ef296b84d44425760fe813cabd7afbb48c8dd62023018b338bbd9d7d6f2f0",
|
||||
"sha256:a2a31ee8a354fa3036d12804730e1e20d58bc4e250365ead34b9c30bbe9908c3",
|
||||
"sha256:a63907332531a499b8cdfd18953febb5a4c525e9e7ca4ac147423b917244b260",
|
||||
"sha256:a8240df4957a5b0e641998a5d78b3c4ea762c845d8cb8997bf820626826fde9a",
|
||||
"sha256:b8806649983a1c78874ec7e04393ef076805740f6319e87a56f91f1767960212",
|
||||
"sha256:c077c9d04a040dba001cf62b3aff08fd85be86bccf2c51a770c77377662a2d55",
|
||||
"sha256:c529ba90c1775697a65b46c83d47a2d3de70f24d96da5d41d05a761c73b063af",
|
||||
"sha256:d537e270b3e6bfaea4f49eaf267984bfb3628c86670e9ad2a257358d3b8f0955",
|
||||
"sha256:d629d750ebe75a88184db98f759633b0a7772c2e6f4da529f0027b4a402c0e2f",
|
||||
"sha256:d9d53518eeed12190744d366ec4a3f39b99d7daa705abca95f87dd8b442df4ad",
|
||||
"sha256:e490f08897cb44e54bddf5c6e27deca9b58c4076849f32aaa7a0b9f1730f2c20",
|
||||
"sha256:f579caecbbca291b0fcc7d473664c8c08635da2f9b1567c22ea32311c86ef68c"
|
||||
],
|
||||
"version": "==2020.10.11"
|
||||
},
|
||||
"toml": {
|
||||
"hashes": [
|
||||
"sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f",
|
||||
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
||||
],
|
||||
"version": "==0.10.1"
|
||||
},
|
||||
"typed-ast": {
|
||||
"hashes": [
|
||||
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
|
||||
"sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
|
||||
"sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
|
||||
"sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
|
||||
"sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
|
||||
"sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
|
||||
"sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
|
||||
"sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
|
||||
"sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
|
||||
"sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
|
||||
"sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
|
||||
"sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
|
||||
"sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
|
||||
"sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
|
||||
"sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
|
||||
"sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
|
||||
"sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
|
||||
"sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
|
||||
"sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
|
||||
"sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
|
||||
"sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
|
||||
],
|
||||
"version": "==1.4.1"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
|
||||
"sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
|
||||
"sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
|
||||
],
|
||||
"version": "==3.7.4.3"
|
||||
}
|
||||
}
|
||||
"develop": {}
|
||||
}
|
||||
|
||||
31
TEMPLATE.env
31
TEMPLATE.env
@@ -33,6 +33,8 @@ ACCOUNT_SIGNUPS_ENABLED=False
|
||||
ACCOUNT_EMAIL_VERIFICATION=none
|
||||
|
||||
# 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
|
||||
|
||||
# Set to "False" if you will not be serving content over HTTPS
|
||||
@@ -70,3 +72,32 @@ SHOW_SHYNET_VERSION=True
|
||||
# that you have a separate queue consumer running somewhere via `celeryworker.sh`.
|
||||
# CELERY_TASK_ALWAYS_EAGER=False
|
||||
# CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
|
||||
|
||||
# Should Shynet show third-party icons in the dashboard?
|
||||
SHOW_THIRD_PARTY_ICONS=True
|
||||
|
||||
# Should Shynet block collection of IP addresses globally?
|
||||
BLOCK_ALL_IPS=False
|
||||
|
||||
# Should Shynet include the date and site ID when hashing users?
|
||||
# This will prevent any possibility of cross-site tracking provided
|
||||
# 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
|
||||
|
||||
# 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.",
|
||||
"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
|
||||
},
|
||||
"USE_RELATIVE_MAX_IN_BAR_VISUALIZATION": {
|
||||
"description": "Should background bars be scaled to full width?",
|
||||
"value": "True",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 215 KiB After Width: | Height: | Size: 263 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 240 KiB |
@@ -61,7 +61,7 @@ spec:
|
||||
selector:
|
||||
app: shynet-redis
|
||||
---
|
||||
apiVersion: apps/v1beta2
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: shynet-redis
|
||||
@@ -83,3 +83,37 @@ spec:
|
||||
ports:
|
||||
- containerPort: 6379
|
||||
name: redis
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: shynet-webserver-service
|
||||
spec:
|
||||
type: ClusterIP
|
||||
ports:
|
||||
- port: 8080
|
||||
selector:
|
||||
app: shynet-webserver
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1beta1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: shynet-webserver-ingress
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: addon-http-application-routing
|
||||
spec:
|
||||
rules:
|
||||
- host: shynet.rmrm.io
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: shynet-webserver-service
|
||||
servicePort: 8080
|
||||
path: /
|
||||
- host: shynet-beta.rmrm.io
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
serviceName: shynet-webserver-service
|
||||
servicePort: 8080
|
||||
path: /
|
||||
1193
package-lock.json
generated
1193
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -18,10 +18,11 @@
|
||||
"homepage": "https://github.com/milesmcc/shynet#readme",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-free": "^5.15.1",
|
||||
"a17t": "^0.2.9",
|
||||
"apexcharts": "^3.22.0",
|
||||
"a17t": "^0.5.1",
|
||||
"apexcharts": "^3.24.0",
|
||||
"datamaps": "^0.5.9",
|
||||
"flag-icon-css": "^3.5.0",
|
||||
"inter-ui": "^3.15.0",
|
||||
"litepicker": "^1.5.7",
|
||||
"turbolinks": "^5.2.0"
|
||||
"litepicker": "^2.0.11"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<nav class="flex w-full flex-wrap items-center justify-between" role="navigation" aria-label="pagination">
|
||||
<div class="w-full md:w-auto mb-2">
|
||||
{% if page.has_previous %}
|
||||
<a href="?page={{ page.previous_page_number }}{{url_parameters}}" class="button field bg-neutral-000 w-auto mr-1">Previous</a>
|
||||
<a href="?page={{ page.previous_page_number }}&{{url_parameters}}" class="button field bg-neutral-000 w-auto mr-1">Previous</a>
|
||||
{% else %}
|
||||
<a class="button field bg-neutral-000 w-auto mr-1" disabled>Previous</a>
|
||||
{% endif %}
|
||||
{% if page.has_next %}
|
||||
<a href="?page={{ page.next_page_number }}{{url_parameters}}" class="button field bg-neutral-000 w-auto">Next</a>
|
||||
<a href="?page={{ page.next_page_number }}&{{url_parameters}}" class="button field bg-neutral-000 w-auto">Next</a>
|
||||
{% else %}
|
||||
<a class="button field bg-neutral-000 w-auto" disabled>Next</a>
|
||||
{% endif %}
|
||||
@@ -17,7 +17,7 @@
|
||||
{% ifequal page.number pnum %}
|
||||
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
|
||||
{% else %}
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
|
||||
{% endifequal %}
|
||||
{% endfor %}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
{% ifequal page.number pnum %}
|
||||
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
|
||||
{% else %}
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
|
||||
{% endifequal %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
@@ -38,7 +38,7 @@
|
||||
{% ifequal page.number pnum %}
|
||||
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
|
||||
{% else %}
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
|
||||
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
|
||||
{% endifequal %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
@@ -15,12 +15,8 @@ def pagination(
|
||||
before_current_pages=4,
|
||||
after_current_pages=4,
|
||||
):
|
||||
url_parameters = "".join(
|
||||
[
|
||||
f"&{urlencode(key)}={urlencode(value)}"
|
||||
for key, value in request.GET.items()
|
||||
if key != "page"
|
||||
]
|
||||
url_parameters = urlencode(
|
||||
[(key, value) for key, value in request.GET.items() if key != "page"]
|
||||
)
|
||||
|
||||
before = max(page.number - before_current_pages - 1, 0)
|
||||
|
||||
@@ -60,7 +60,9 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
],
|
||||
options={"ordering": ["-start_time"],},
|
||||
options={
|
||||
"ordering": ["-start_time"],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Hit",
|
||||
@@ -90,7 +92,9 @@ class Migration(migrations.Migration):
|
||||
),
|
||||
),
|
||||
],
|
||||
options={"ordering": ["-start_time"],},
|
||||
options={
|
||||
"ordering": ["-start_time"],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="session",
|
||||
|
||||
46
shynet/analytics/migrations/0004_auto_20210328_1514.py
Normal file
46
shynet/analytics/migrations/0004_auto_20210328_1514.py
Normal file
@@ -0,0 +1,46 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-28 19:14
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("analytics", "0003_auto_20200502_1227"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="hit",
|
||||
name="last_seen",
|
||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="hit",
|
||||
name="start_time",
|
||||
field=models.DateTimeField(
|
||||
db_index=True, default=django.utils.timezone.now
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="last_seen",
|
||||
field=models.DateTimeField(
|
||||
db_index=True, default=django.utils.timezone.now
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="start_time",
|
||||
field=models.DateTimeField(
|
||||
db_index=True, default=django.utils.timezone.now
|
||||
),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name="session",
|
||||
index=models.Index(
|
||||
fields=["service", "-last_seen"], name="analytics_s_service_10bb96_idx"
|
||||
),
|
||||
),
|
||||
]
|
||||
26
shynet/analytics/migrations/0005_auto_20210328_1518.py
Normal file
26
shynet/analytics/migrations/0005_auto_20210328_1518.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-28 19:18
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("analytics", "0004_auto_20210328_1514"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="hit",
|
||||
name="last_seen",
|
||||
field=models.DateTimeField(
|
||||
db_index=True, default=django.utils.timezone.now
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="hit",
|
||||
name="load_time",
|
||||
field=models.FloatField(db_index=True, null=True),
|
||||
),
|
||||
]
|
||||
40
shynet/analytics/migrations/0006_hit_service.py
Normal file
40
shynet/analytics/migrations/0006_hit_service.py
Normal file
@@ -0,0 +1,40 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-28 19:36
|
||||
|
||||
from ..models import Hit, Session
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.db.models import Subquery, OuterRef
|
||||
|
||||
|
||||
def add_service_to_hits(_a, _b):
|
||||
service = Session.objects.filter(pk=OuterRef("session")).values_list("service")[:1]
|
||||
|
||||
Hit.objects.update(service=Subquery(service))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0008_auto_20200628_1403"),
|
||||
("analytics", "0005_auto_20210328_1518"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="hit",
|
||||
name="service",
|
||||
field=models.ForeignKey(
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="core.service",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(add_service_to_hits, lambda: ()),
|
||||
migrations.AlterField(
|
||||
model_name="hit",
|
||||
name="service",
|
||||
field=models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE, to="core.service"
|
||||
),
|
||||
),
|
||||
]
|
||||
19
shynet/analytics/migrations/0007_auto_20210328_1634.py
Normal file
19
shynet/analytics/migrations/0007_auto_20210328_1634.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-28 20:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("analytics", "0006_hit_service"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name="hit",
|
||||
index=models.Index(
|
||||
fields=["service", "-start_time"], name="analytics_h_service_f4f41e_idx"
|
||||
),
|
||||
),
|
||||
]
|
||||
18
shynet/analytics/migrations/0008_session_is_bounce.py
Normal file
18
shynet/analytics/migrations/0008_session_is_bounce.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.7 on 2021-03-28 21:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("analytics", "0007_auto_20210328_1634"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="session",
|
||||
name="is_bounce",
|
||||
field=models.BooleanField(db_index=True, default=True),
|
||||
)
|
||||
]
|
||||
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,11 +1,10 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
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():
|
||||
@@ -20,8 +19,8 @@ class Session(models.Model):
|
||||
identifier = models.TextField(blank=True, db_index=True)
|
||||
|
||||
# Time
|
||||
start_time = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
last_seen = models.DateTimeField(auto_now_add=True)
|
||||
start_time = models.DateTimeField(default=timezone.now, db_index=True)
|
||||
last_seen = models.DateTimeField(default=timezone.now, db_index=True)
|
||||
|
||||
# Core request information
|
||||
user_agent = models.TextField()
|
||||
@@ -48,16 +47,19 @@ class Session(models.Model):
|
||||
latitude = models.FloatField(null=True)
|
||||
time_zone = models.TextField(blank=True)
|
||||
|
||||
is_bounce = models.BooleanField(default=True, db_index=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-start_time"]
|
||||
indexes = [
|
||||
models.Index(fields=["service", "-start_time"]),
|
||||
models.Index(fields=["service", "-last_seen"]),
|
||||
models.Index(fields=["service", "identifier"]),
|
||||
]
|
||||
|
||||
@property
|
||||
def is_currently_active(self):
|
||||
return timezone.now() - self.last_seen < timezone.timedelta(seconds=10)
|
||||
return timezone.now() - self.last_seen < ACTIVE_USER_TIMEDELTA
|
||||
|
||||
@property
|
||||
def duration(self):
|
||||
@@ -72,14 +74,20 @@ class Session(models.Model):
|
||||
kwargs={"pk": self.service.pk, "session_pk": self.uuid},
|
||||
)
|
||||
|
||||
def recalculate_bounce(self):
|
||||
bounce = self.hit_set.count() == 1
|
||||
if bounce != self.is_bounce:
|
||||
self.is_bounce = bounce
|
||||
self.save()
|
||||
|
||||
|
||||
class Hit(models.Model):
|
||||
session = models.ForeignKey(Session, on_delete=models.CASCADE, db_index=True)
|
||||
initial = models.BooleanField(default=True, db_index=True)
|
||||
|
||||
# Base request information
|
||||
start_time = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
last_seen = models.DateTimeField(auto_now_add=True)
|
||||
start_time = models.DateTimeField(default=timezone.now, db_index=True)
|
||||
last_seen = models.DateTimeField(default=timezone.now, db_index=True)
|
||||
heartbeats = models.IntegerField(default=0)
|
||||
tracker = models.TextField(
|
||||
choices=[("JS", "JavaScript"), ("PIXEL", "Pixel (noscript)")]
|
||||
@@ -88,12 +96,17 @@ class Hit(models.Model):
|
||||
# Advanced page information
|
||||
location = models.TextField(blank=True, db_index=True)
|
||||
referrer = models.TextField(blank=True, db_index=True)
|
||||
load_time = models.FloatField(null=True)
|
||||
load_time = models.FloatField(null=True, db_index=True)
|
||||
|
||||
# While not necessary, we store the root service directly for performance.
|
||||
# It makes querying much easier; no need for inner joins.
|
||||
service = models.ForeignKey(Service, on_delete=models.CASCADE, db_index=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-start_time"]
|
||||
indexes = [
|
||||
models.Index(fields=["session", "-start_time"]),
|
||||
models.Index(fields=["service", "-start_time"]),
|
||||
models.Index(fields=["session", "location"]),
|
||||
models.Index(fields=["session", "referrer"]),
|
||||
]
|
||||
@@ -105,5 +118,5 @@ class Hit(models.Model):
|
||||
def get_absolute_url(self):
|
||||
return reverse(
|
||||
"dashboard:service_session",
|
||||
kwargs={"pk": self.session.service.pk, "session_pk": self.session.pk},
|
||||
kwargs={"pk": self.service.pk, "session_pk": self.session.pk},
|
||||
)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import ipaddress
|
||||
import json
|
||||
import logging
|
||||
from hashlib import sha256
|
||||
|
||||
@@ -40,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
|
||||
@@ -59,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:
|
||||
@@ -68,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)
|
||||
@@ -79,6 +83,11 @@ def ingress_request(
|
||||
association_id_hash = sha256()
|
||||
association_id_hash.update(str(ip).encode("utf-8"))
|
||||
association_id_hash.update(str(user_agent).encode("utf-8"))
|
||||
if settings.AGGRESSIVE_HASH_SALTING:
|
||||
association_id_hash.update(str(service.pk).encode("utf-8"))
|
||||
association_id_hash.update(
|
||||
str(timezone.now().date().isoformat()).encode("utf-8")
|
||||
)
|
||||
session_cache_path = (
|
||||
f"session_association_{service.pk}_{association_id_hash.hexdigest()}"
|
||||
)
|
||||
@@ -117,12 +126,14 @@ def ingress_request(
|
||||
return
|
||||
session = Session.objects.create(
|
||||
service=service,
|
||||
ip=ip if service.collect_ips else None,
|
||||
ip=ip if service.collect_ips and not settings.BLOCK_ALL_IPS else None,
|
||||
user_agent=user_agent,
|
||||
identifier=identifier.strip(),
|
||||
browser=ua.browser.family or "",
|
||||
device=ua.device.family or ua.device.model or "",
|
||||
device_type=device_type,
|
||||
start_time=time,
|
||||
last_seen=time,
|
||||
os=ua.os.family or "",
|
||||
asn=ip_data.get("asn") or "",
|
||||
country=ip_data.get("country") or "",
|
||||
@@ -139,7 +150,7 @@ def ingress_request(
|
||||
log.debug("Updating old session with new data...")
|
||||
|
||||
# Update last seen time
|
||||
session.last_seen = timezone.now()
|
||||
session.last_seen = time
|
||||
if session.identifier == "" and identifier.strip() != "":
|
||||
session.identifier = identifier.strip()
|
||||
session.save()
|
||||
@@ -160,7 +171,7 @@ def ingress_request(
|
||||
# this is a heartbeat.
|
||||
log.debug("Hit is a heartbeat; updating old hit with new data...")
|
||||
hit.heartbeats += 1
|
||||
hit.last_seen = timezone.now()
|
||||
hit.last_seen = time
|
||||
hit.save()
|
||||
|
||||
if hit is None:
|
||||
@@ -176,7 +187,14 @@ def ingress_request(
|
||||
location=payload.get("location", location),
|
||||
referrer=payload.get("referrer", ""),
|
||||
load_time=payload.get("loadTime"),
|
||||
start_time=time,
|
||||
last_seen=time,
|
||||
service=service,
|
||||
)
|
||||
|
||||
# Recalculate whether the session is a bounce
|
||||
session.recalculate_bounce()
|
||||
|
||||
# Set idempotency (if applicable)
|
||||
if idempotency is not None:
|
||||
cache.set(
|
||||
@@ -184,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 %}
|
||||
|
||||
@@ -5,7 +5,12 @@ from urllib.parse import urlparse
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.http import (
|
||||
Http404,
|
||||
HttpResponse,
|
||||
HttpResponseBadRequest,
|
||||
HttpResponseForbidden,
|
||||
)
|
||||
from django.shortcuts import render, reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -49,21 +54,24 @@ 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")
|
||||
if remote_origin is None and request.META.get("HTTP_REFERER") is not None:
|
||||
if (
|
||||
remote_origin is None
|
||||
and request.META.get("HTTP_REFERER") is not None
|
||||
):
|
||||
parsed = urlparse(request.META.get("HTTP_REFERER"))
|
||||
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"
|
||||
@@ -104,7 +112,9 @@ class ScriptView(ValidateServiceOriginsMixin, View):
|
||||
endpoint = (
|
||||
reverse(
|
||||
"ingress:endpoint_script",
|
||||
kwargs={"service_uuid": self.kwargs.get("service_uuid"),},
|
||||
kwargs={
|
||||
"service_uuid": self.kwargs.get("service_uuid"),
|
||||
},
|
||||
)
|
||||
if self.kwargs.get("identifier") == None
|
||||
else reverse(
|
||||
|
||||
117
shynet/core/management/commands/demo.py
Normal file
117
shynet/core/management/commands/demo.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import traceback
|
||||
from django.utils.timezone import now
|
||||
from django.utils.timezone import timedelta
|
||||
import random
|
||||
import uuid
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils.crypto import get_random_string
|
||||
import user_agents
|
||||
from logging import info
|
||||
|
||||
from core.models import User, Service
|
||||
from analytics.models import Session, Hit
|
||||
from analytics.tasks import ingress_request
|
||||
|
||||
LOCATIONS = [
|
||||
"/",
|
||||
"/post/{rand}",
|
||||
"/login",
|
||||
"/me",
|
||||
]
|
||||
|
||||
REFERRERS = [
|
||||
"https://news.ycombinator.com/item?id=11116274",
|
||||
"https://news.ycombinator.com/item?id=24872911",
|
||||
"https://reddit.com",
|
||||
"https://facebook.com",
|
||||
"https://twitter.com/milesmccain",
|
||||
"https://twitter.com",
|
||||
"https://stanford.edu/~mccain/",
|
||||
"https://tiktok.com",
|
||||
"https://io.stanford.edu",
|
||||
"https://en.wikipedia.org",
|
||||
"https://stackoverflow.com",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
]
|
||||
|
||||
USER_AGENTS = [
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36",
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/43.4",
|
||||
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36",
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 11_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko)",
|
||||
"Version/10.0 Mobile/14E304 Safari/602.1",
|
||||
]
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Configures a Shynet demo service"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"name",
|
||||
type=str,
|
||||
)
|
||||
parser.add_argument("owner_email", type=str)
|
||||
parser.add_argument(
|
||||
"avg",
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument("deviation", type=float, default=0.4)
|
||||
parser.add_argument(
|
||||
"days",
|
||||
type=int,
|
||||
)
|
||||
parser.add_argument("load_time", type=float, default=1000)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
owner = User.objects.get(email=options.get("owner_email"))
|
||||
service = Service.objects.create(name=options.get("name"), owner=owner)
|
||||
|
||||
print(
|
||||
f"Created demo service `{service.name}` (uuid: `{service.uuid}`, owner: {owner})"
|
||||
)
|
||||
|
||||
# Go through each day requested, creating sessions and hits
|
||||
for days in range(options.get("days")):
|
||||
day = (now() - timedelta(days=days)).replace(hour=0, minute=0, second=0)
|
||||
print(f"Populating info for {day}...")
|
||||
avg = options.get("avg")
|
||||
deviation = options.get("deviation")
|
||||
ips = [
|
||||
".".join(map(str, (random.randint(0, 255) for _ in range(4))))
|
||||
for _ in range(avg)
|
||||
]
|
||||
|
||||
n = avg + random.randrange(-1 * deviation * avg, deviation * avg)
|
||||
for _ in range(n):
|
||||
time = day + timedelta(
|
||||
hours=random.randrange(0, 23),
|
||||
minutes=random.randrange(0, 59),
|
||||
seconds=random.randrange(0, 59),
|
||||
)
|
||||
ip = random.choice(ips)
|
||||
load_time = random.normalvariate(options.get("load_time"), 500)
|
||||
referrer = random.choice(REFERRERS)
|
||||
location = "https://example.com" + random.choice(LOCATIONS).replace(
|
||||
"{rand}", str(random.randint(0, n))
|
||||
)
|
||||
user_agent = random.choice(USER_AGENTS)
|
||||
ingress_request(
|
||||
service.uuid,
|
||||
"JS",
|
||||
time,
|
||||
{"loadTime": load_time, "referrer": referrer},
|
||||
ip,
|
||||
location,
|
||||
user_agent,
|
||||
)
|
||||
|
||||
print(f"Created {n} demo hits on {day}!")
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Successfully created demo data!"))
|
||||
@@ -14,7 +14,8 @@ class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"hostname", type=str,
|
||||
"hostname",
|
||||
type=str,
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
@@ -14,7 +14,8 @@ class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"email", type=str,
|
||||
"email",
|
||||
type=str,
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
@@ -14,7 +14,8 @@ class Command(BaseCommand):
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"name", type=str,
|
||||
"name",
|
||||
type=str,
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
|
||||
@@ -112,7 +112,9 @@ class Migration(migrations.Migration):
|
||||
"verbose_name_plural": "users",
|
||||
"abstract": False,
|
||||
},
|
||||
managers=[("objects", django.contrib.auth.models.UserManager()),],
|
||||
managers=[
|
||||
("objects", django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="Service",
|
||||
|
||||
@@ -11,6 +11,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name="service", options={"ordering": ["name", "uuid"]},
|
||||
name="service",
|
||||
options={"ordering": ["name", "uuid"]},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -6,18 +6,20 @@ from django.db import migrations, models
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0007_service_ignore_robots'),
|
||||
("core", "0007_service_ignore_robots"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='service',
|
||||
name='script_inject',
|
||||
field=models.TextField(blank=True, default=''),
|
||||
model_name="service",
|
||||
name="script_inject",
|
||||
field=models.TextField(blank=True, default=""),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='first_name',
|
||||
field=models.CharField(blank=True, max_length=150, verbose_name='first name'),
|
||||
model_name="user",
|
||||
name="first_name",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=150, verbose_name="first name"
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
@@ -129,11 +137,13 @@ class Service(models.Model):
|
||||
session_count = sessions.count()
|
||||
|
||||
hits = Hit.objects.filter(
|
||||
session__service=self, start_time__lt=end_time, start_time__gt=start_time
|
||||
service=self, start_time__lt=end_time, start_time__gt=start_time
|
||||
)
|
||||
hit_count = hits.count()
|
||||
|
||||
bounces = sessions.annotate(hit_count=models.Count("hit")).filter(hit_count=1)
|
||||
has_hits = Hit.objects.filter(service=self).exists()
|
||||
|
||||
bounces = sessions.filter(is_bounce=True)
|
||||
bounce_count = bounces.count()
|
||||
|
||||
locations = (
|
||||
@@ -188,6 +198,36 @@ class Service(models.Model):
|
||||
|
||||
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:
|
||||
avg_session_duration = sessions.annotate(
|
||||
duration=models.F("last_seen") - models.F("start_time")
|
||||
@@ -202,46 +242,74 @@ 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
|
||||
return avg_session_duration
|
||||
|
||||
return {
|
||||
"currently_online": currently_online,
|
||||
"session_count": session_count,
|
||||
"hit_count": hit_count,
|
||||
"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,
|
||||
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")
|
||||
.annotate(count=models.Count("uuid"))
|
||||
.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):
|
||||
day = (start_time + timezone.timedelta(days=day_offset)).date()
|
||||
if day not in chart_data and day <= tz_now.date():
|
||||
chart_data[day] = {"sessions": 0, "hits": 0}
|
||||
|
||||
chart_data = sorted(chart_data.items(), key=lambda k: k[0])
|
||||
chart_data = {
|
||||
'sessions': [v['sessions'] for k, v in chart_data],
|
||||
'hits': [v['hits'] for k, v in chart_data],
|
||||
'labels': [str(k) for k, v in chart_data],
|
||||
}
|
||||
|
||||
return chart_data, chart_tooltip_format, chart_granularity
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse("dashboard:service", kwargs={"pk": self.pk},)
|
||||
return reverse(
|
||||
"dashboard:service",
|
||||
kwargs={"pk": self.pk},
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from allauth.account.admin import EmailAddress
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.models import Service, User
|
||||
@@ -18,7 +19,7 @@ class ServiceForm(forms.ModelForm):
|
||||
"hide_referrer_regex",
|
||||
"origins",
|
||||
"collaborators",
|
||||
"script_inject"
|
||||
"script_inject",
|
||||
]
|
||||
widgets = {
|
||||
"name": forms.TextInput(),
|
||||
@@ -28,12 +29,11 @@ class ServiceForm(forms.ModelForm):
|
||||
"collect_ips": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
|
||||
"ignore_robots": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
|
||||
"hide_referrer_regex": forms.TextInput(),
|
||||
"script_inject": forms.Textarea(attrs={'class':'font-mono', 'rows': 5})
|
||||
"script_inject": forms.Textarea(attrs={"class": "font-mono", "rows": 5}),
|
||||
}
|
||||
labels = {
|
||||
"origins": "Allowed origins",
|
||||
"respect_dnt": "Respect DNT",
|
||||
"collect_ips": "Collect IP addresses",
|
||||
"ignored_ips": "Ignored IP addresses",
|
||||
"ignore_robots": "Ignore robots",
|
||||
"hide_referrer_regex": "Hide specific referrers",
|
||||
@@ -46,13 +46,27 @@ class ServiceForm(forms.ModelForm):
|
||||
"At what origins does the service operate? Use commas to separate multiple values. This sets CORS headers, so use '*' if you're not sure (or don't care)."
|
||||
),
|
||||
"respect_dnt": "Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?",
|
||||
"collect_ips": "Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected.",
|
||||
"ignored_ips": "A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32').",
|
||||
"ignore_robots": "Should sessions generated by bots be excluded from tracking?",
|
||||
"hide_referrer_regex": "Any referrers that match this <a href='https://regexr.com/'>RegEx</a> will not be listed in the referrer summary. Sessions will still be tracked normally. No effect if left blank.",
|
||||
"script_inject": "Optional additional JavaScript to inject at the end of the Shynet script. This code will be injected on every page where this service is installed.",
|
||||
}
|
||||
|
||||
collect_ips = forms.BooleanField(
|
||||
help_text="IP address collection is disabled globally by your administrator."
|
||||
if settings.BLOCK_ALL_IPS
|
||||
else "Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected.",
|
||||
widget=forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
|
||||
initial=False if settings.BLOCK_ALL_IPS else True,
|
||||
required=False,
|
||||
disabled=settings.BLOCK_ALL_IPS,
|
||||
)
|
||||
|
||||
def clean_collect_ips(self):
|
||||
collect_ips = self.cleaned_data["collect_ips"]
|
||||
# Forces collect IPs to be false if it is disabled globally
|
||||
return False if settings.BLOCK_ALL_IPS else collect_ips
|
||||
|
||||
collaborators = forms.CharField(
|
||||
help_text="Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)",
|
||||
required=False,
|
||||
@@ -60,7 +74,9 @@ class ServiceForm(forms.ModelForm):
|
||||
|
||||
def clean_collaborators(self):
|
||||
collaborators = []
|
||||
users_to_emails = {} # maps users to the email they are listed under as a collaborator
|
||||
users_to_emails = (
|
||||
{}
|
||||
) # maps users to the email they are listed under as a collaborator
|
||||
for collaborator_email in self.cleaned_data["collaborators"].split(","):
|
||||
email = collaborator_email.strip()
|
||||
if email == "":
|
||||
@@ -72,7 +88,9 @@ class ServiceForm(forms.ModelForm):
|
||||
raise forms.ValidationError(f"Email '{email}' is not registered")
|
||||
user = collaborator_email_linked.user
|
||||
if user in collaborators:
|
||||
raise forms.ValidationError(f"The emails '{email}' and '{users_to_emails[user]}' both correspond to the same user")
|
||||
raise forms.ValidationError(
|
||||
f"The emails '{email}' and '{users_to_emails[user]}' both correspond to the same user"
|
||||
)
|
||||
users_to_emails[user] = email
|
||||
collaborators.append(collaborator_email_linked.user)
|
||||
return collaborators
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,33 @@
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.force-limited-height {
|
||||
max-height: 400px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rf {
|
||||
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;
|
||||
--color-neutral-100: #F1F5F9;
|
||||
--color-neutral-200: #E2E8F0;
|
||||
--color-neutral-300: #CBD5E1;
|
||||
--color-neutral-400: #94A3B8;
|
||||
--color-neutral-500: #64748B;
|
||||
--color-neutral-600: #475569;
|
||||
--color-neutral-700: #334155;
|
||||
--color-neutral-800: #1E293B;
|
||||
--color-neutral-900: #0F172A;
|
||||
}
|
||||
BIN
shynet/dashboard/static/dashboard/images/icon.png
Normal file
BIN
shynet/dashboard/static/dashboard/images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -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 %}
|
||||
|
||||
@@ -8,31 +8,38 @@
|
||||
<meta name="robots" content="noindex">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
{% 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 'turbolinks/dist/turbolinks.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>
|
||||
<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 'litepicker/dist/css/litepicker.css' %}">
|
||||
{% block extra_head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
<body class="bg-neutral-200 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">
|
||||
<aside
|
||||
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">
|
||||
@@ -40,8 +47,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:20 url=url %}
|
||||
{% 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 %}
|
||||
@@ -59,8 +66,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">
|
||||
@@ -120,4 +127,4 @@
|
||||
{% endblock %}
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
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="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" style="max-width: 200px;" 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>
|
||||
|
||||
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,15 +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">
|
||||
{{object.name}}
|
||||
<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 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">
|
||||
@@ -50,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.chart_data sparkline=True height=100 name=object.uuid tooltip_format=stats.chart_tooltip_format granularity=stats.chart_granularity %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
</a>
|
||||
</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 %}"
|
||||
{% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{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 %}
|
||||
|
||||
@@ -5,9 +5,14 @@
|
||||
enabled: false
|
||||
},
|
||||
tooltip: {
|
||||
shared: false,
|
||||
shared: true,
|
||||
x: {
|
||||
format: '{{tooltip_format|default:"MMM d"}}',
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
colors: ["#805AD5"],
|
||||
chart: {
|
||||
zoom: {
|
||||
enabled: false,
|
||||
@@ -15,7 +20,7 @@
|
||||
toolbar: {
|
||||
show: false,
|
||||
},
|
||||
type: 'area',
|
||||
type: 'line',
|
||||
height: {{height|default:"200"}},
|
||||
offsetY: -1,
|
||||
animations: {
|
||||
@@ -24,16 +29,14 @@
|
||||
sparkline: {
|
||||
enabled: {% if sparkline %}true{% else %}false{% endif %},
|
||||
},
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: {
|
||||
shadeIntensity: 1,
|
||||
inverseColors: false,
|
||||
opacityFrom: 0.8,
|
||||
opacityTo: 0,
|
||||
stops: [0, 75, 100]
|
||||
{% if granularity == "daily" and click_zoom %}
|
||||
events: {
|
||||
markerClick: function(event, chartContext, { seriesIndex, dataPointIndex, w: {config}}) {
|
||||
const day = config.labels[dataPointIndex]
|
||||
window.location.href = `?startDate=${day}&endDate=${day}`
|
||||
},
|
||||
},
|
||||
{% endif %}
|
||||
},
|
||||
grid: {
|
||||
padding: {
|
||||
@@ -63,15 +66,27 @@
|
||||
},
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
labels: {
|
||||
datetimeUTC: false
|
||||
},
|
||||
},
|
||||
stroke: {
|
||||
width: 1.5,
|
||||
width: 2,
|
||||
curve: 'smooth',
|
||||
},
|
||||
series: [{
|
||||
name: "{{unit|default:'Sessions'}}",
|
||||
data: {{data|safe}}
|
||||
}]
|
||||
name: "Hits",
|
||||
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);
|
||||
triggerMatchesChart.render();
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load rules %}
|
||||
{% load rules pagination %}
|
||||
|
||||
{% 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,10 +17,15 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr class="sep">
|
||||
{% for object in services|dictsortreversed:"stats.session_count" %}
|
||||
<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,29 +6,37 @@
|
||||
<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="">
|
||||
<p class="label text-gray-400">Sessions</p>
|
||||
<p class="heading">
|
||||
{{stats.session_count|intcomma}}
|
||||
<div>
|
||||
{% compare stats.compare.session_count stats.session_count "UP" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
<div>
|
||||
{% compare stats.compare.session_count stats.session_count "UP" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
</p>
|
||||
</article>
|
||||
<article class="">
|
||||
<p class="label text-gray-400">Hits</p>
|
||||
<p class="heading">
|
||||
{{stats.hit_count|intcomma}}
|
||||
<div>
|
||||
{% compare stats.compare.hit_count stats.hit_count "UP" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
<div>
|
||||
{% compare stats.compare.hit_count stats.hit_count "UP" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
</p>
|
||||
</article>
|
||||
<article class="">
|
||||
@@ -39,9 +47,9 @@
|
||||
{% else %}
|
||||
?
|
||||
{% endif %}
|
||||
<div>
|
||||
{% compare stats.compare.avg_load_time stats.avg_load_time "DOWN" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
<div>
|
||||
{% compare stats.compare.avg_load_time stats.avg_load_time "DOWN" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
</p>
|
||||
</article>
|
||||
<article class="">
|
||||
@@ -52,9 +60,9 @@
|
||||
{% else %}
|
||||
?
|
||||
{% endif %}
|
||||
<div>
|
||||
{% compare stats.compare.bounce_rate_pct stats.bounce_rate_pct "DOWN" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
<div>
|
||||
{% compare stats.compare.bounce_rate_pct stats.bounce_rate_pct "DOWN" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
</p>
|
||||
</article>
|
||||
<article class="">
|
||||
@@ -65,9 +73,9 @@
|
||||
{% else %}
|
||||
?
|
||||
{% endif %}
|
||||
<div>
|
||||
{% compare stats.compare.avg_session_duration stats.avg_session_duration "UP" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
<div>
|
||||
{% compare stats.compare.avg_session_duration stats.avg_session_duration "UP" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
</p>
|
||||
</article>
|
||||
<article class="">
|
||||
@@ -78,16 +86,17 @@
|
||||
{% else %}
|
||||
?
|
||||
{% endif %}
|
||||
<div>
|
||||
{% compare stats.compare.avg_hits_per_session stats.avg_hits_per_session "UP" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
<div>
|
||||
{% compare stats.compare.avg_hits_per_session stats.avg_hits_per_session "UP" classes=classes good_classes=good_classes bad_classes=bad_classes neutral_classes=neutral_classes %}
|
||||
</div>
|
||||
</p>
|
||||
</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.chart_data tooltip_format=stats.chart_tooltip_format granularity=stats.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,8 +109,20 @@
|
||||
<tbody>
|
||||
{% for location in stats.locations %}
|
||||
<tr>
|
||||
<td>{{location.location|default:"Unknown"|urldisplay}}</td>
|
||||
<td class="rf">{{location.count|intcomma}}</td>
|
||||
<td class="truncate w-full max-w-0 relative">
|
||||
{% 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>
|
||||
{% empty %}
|
||||
<tr>
|
||||
@@ -111,6 +132,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</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">
|
||||
<table class="table">
|
||||
<thead class="text-sm">
|
||||
@@ -122,30 +147,20 @@
|
||||
<tbody>
|
||||
{% for referrer in stats.referrers %}
|
||||
<tr>
|
||||
<td>{{referrer.referrer|default:"Direct"|urldisplay}}</td>
|
||||
<td class="rf">{{referrer.count|intcomma}}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td><span class="text-gray-600">No data yet...</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
<table class="table">
|
||||
<thead class="text-sm">
|
||||
<tr>
|
||||
<th>Country</th>
|
||||
<th class="rf">Sessions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for country in stats.countries %}
|
||||
<tr>
|
||||
<td>{{country.country|flag_emoji}} {{country.country|country_name}}</td>
|
||||
<td class="rf">{{country.count|intcomma}}</td>
|
||||
<td class="truncate w-full max-w-0 relative">
|
||||
{% include 'dashboard/includes/bar.html' with count=referrer.count max=stats.referrers.0.count total=stats.session_count %}
|
||||
<div class="relative flex items-center">
|
||||
{{referrer.referrer|default:"Direct"|urldisplay}}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="flex justify-end items-center">
|
||||
{{referrer.count|intcomma}}
|
||||
<span class="text-xs rf" style="min-width: 48px">
|
||||
({{referrer.count|percent:stats.session_count}})
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
@@ -166,8 +181,20 @@
|
||||
<tbody>
|
||||
{% for os in stats.operating_systems %}
|
||||
<tr>
|
||||
<td>{{os.os|default:"Unknown"}}</td>
|
||||
<td class="rf">{{os.count|intcomma}}</td>
|
||||
<td class="flex items-center truncate w-full max-w-0 relative" title="{{os.os|default:'Unknown'}}">
|
||||
{% 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>
|
||||
{% empty %}
|
||||
<tr>
|
||||
@@ -188,8 +215,21 @@
|
||||
<tbody>
|
||||
{% for browser in stats.browsers %}
|
||||
<tr>
|
||||
<td>{{browser.browser|default:"Unknown"}}</td>
|
||||
<td class="rf">{{browser.count|intcomma}}</td>
|
||||
<td class="flex items-center truncate w-full max-w-0 relative" title="{{browser.browser|default:'Unknown'}}">
|
||||
{% include 'dashboard/includes/bar.html' with count=browser.count max=stats.browsers.0.count total=stats.session_count %}
|
||||
</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>
|
||||
{% empty %}
|
||||
<tr>
|
||||
@@ -210,8 +250,20 @@
|
||||
<tbody>
|
||||
{% for device_type in stats.device_types %}
|
||||
<tr>
|
||||
<td>{{device_type.device_type|default:"Unknown"|title}}</td>
|
||||
<td class="rf">{{device_type.count|intcomma}}</td>
|
||||
<td class="truncate w-full max-w-0 relative">
|
||||
{% 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>
|
||||
{% empty %}
|
||||
<tr>
|
||||
@@ -222,10 +274,11 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card ~neutral !low limited-height py-2">
|
||||
<div class="card ~neutral !low 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 sessions
|
||||
<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>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -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 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">
|
||||
@@ -36,4 +31,4 @@
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,20 +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">
|
||||
{{object.name}}
|
||||
<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}}
|
||||
<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,12 +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()
|
||||
|
||||
@@ -26,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
|
||||
@@ -41,9 +43,22 @@ def country_name(isocode):
|
||||
return "Unknown"
|
||||
|
||||
|
||||
@register.filter
|
||||
def datamap_id(isocode):
|
||||
try:
|
||||
return pycountry.countries.get(alpha_2=isocode).alpha_3
|
||||
except:
|
||||
return "UNKNOWN"
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def relative_stat_tone(
|
||||
start, end, good="UP", good_classes=None, bad_classes=None, neutral_classes=None,
|
||||
start,
|
||||
end,
|
||||
good="UP",
|
||||
good_classes=None,
|
||||
bad_classes=None,
|
||||
neutral_classes=None,
|
||||
):
|
||||
good_classes = good_classes or "~positive"
|
||||
bad_classes = bad_classes or "~critical"
|
||||
@@ -73,7 +88,7 @@ def percent_change_display(start, end):
|
||||
elif start == 0:
|
||||
pct_change = "0%"
|
||||
else:
|
||||
change = int(round(100 * abs(end - start) / start))
|
||||
change = int(round(100 * abs(end - start) / max(start, 1)))
|
||||
if change > 999:
|
||||
return "> 999%"
|
||||
else:
|
||||
@@ -84,8 +99,7 @@ def percent_change_display(start, end):
|
||||
|
||||
@register.inclusion_tag("dashboard/includes/sidebar_footer.html")
|
||||
def sidebar_footer():
|
||||
return {"version": "" if settings.SHOW_SHYNET_VERSION
|
||||
else settings.VERSION}
|
||||
return {"version": settings.VERSION if settings.SHOW_SHYNET_VERSION else ""}
|
||||
|
||||
|
||||
@register.inclusion_tag("dashboard/includes/stat_comparison.html")
|
||||
@@ -98,6 +112,12 @@ def compare(
|
||||
bad_classes=None,
|
||||
neutral_classes=None,
|
||||
):
|
||||
if isinstance(start, timedelta):
|
||||
start = start.seconds
|
||||
|
||||
if isinstance(end, timedelta):
|
||||
end = end.seconds
|
||||
|
||||
return {
|
||||
"start": start,
|
||||
"end": end,
|
||||
@@ -116,12 +136,140 @@ def startswith(text, starts):
|
||||
return False
|
||||
|
||||
|
||||
@register.filter
|
||||
def iconify(text):
|
||||
if not settings.SHOW_THIRD_PARTY_ICONS:
|
||||
return ""
|
||||
|
||||
text = text.lower()
|
||||
icons = {
|
||||
"chrome": "chrome.com",
|
||||
"safari": "www.apple.com",
|
||||
"windows": "windows.com",
|
||||
"edge": "microsoft.com",
|
||||
"firefox": "firefox.com",
|
||||
"opera": "opera.com",
|
||||
"unknown": "example.com",
|
||||
"linux": "kernel.org",
|
||||
"ios": "www.apple.com",
|
||||
"mac": "www.apple.com",
|
||||
"macos": "www.apple.com",
|
||||
"mac os x": "www.apple.com",
|
||||
"android": "android.com",
|
||||
"chrome os": "chrome.com",
|
||||
"ubuntu": "ubuntu.com",
|
||||
"fedora": "getfedora.org",
|
||||
"mobile safari": "www.apple.com",
|
||||
"chrome mobile ios": "chrome.com",
|
||||
"chrome mobile": "chrome.com",
|
||||
"samsung internet": "samsung.com",
|
||||
"google": "google.com",
|
||||
"chrome mobile webview": "chrome.com",
|
||||
"firefox mobile": "firefox.com",
|
||||
"edge mobile": "microsoft.com",
|
||||
"chromium": "chromium.org",
|
||||
}
|
||||
|
||||
domain = None
|
||||
if text.startswith("http"):
|
||||
domain = urlparse(text).netloc
|
||||
elif text in icons:
|
||||
domain = icons[text]
|
||||
else:
|
||||
# This fallback works better than you'd think!
|
||||
domain = text + ".com"
|
||||
|
||||
return SafeString(
|
||||
f'<span class="icon mr-1 flex-none"><img src="https://icons.duckduckgo.com/ip3/{domain}.ico"></span>'
|
||||
)
|
||||
|
||||
|
||||
@register.filter
|
||||
def urldisplay(url):
|
||||
if url.startswith("http"):
|
||||
display_url = url.replace("http://", "").replace("https://", "")
|
||||
return SafeString(
|
||||
f"<a href='{url}' title='{url}' rel='nofollow'>{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)
|
||||
)
|
||||
|
||||
|
||||
@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%}'
|
||||
|
||||
@@ -22,16 +22,24 @@ from .forms import ServiceForm
|
||||
from .mixins import DateRangeMixin
|
||||
|
||||
|
||||
class DashboardView(LoginRequiredMixin, DateRangeMixin, TemplateView):
|
||||
class DashboardView(LoginRequiredMixin, DateRangeMixin, ListView):
|
||||
model = Service
|
||||
template_name = "dashboard/pages/dashboard.html"
|
||||
paginate_by = settings.DASHBOARD_PAGE_SIZE
|
||||
|
||||
def get_queryset(self):
|
||||
return Service.objects.filter(
|
||||
Q(owner=self.request.user) | Q(collaborators__in=[self.request.user])
|
||||
).distinct()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
data = super().get_context_data(**kwargs)
|
||||
data["services"] = Service.objects.filter(
|
||||
Q(owner=self.request.user) | Q(collaborators__in=[self.request.user])
|
||||
).distinct()
|
||||
for service in data["services"]:
|
||||
service.stats = service.get_core_stats(data["start_date"], data["end_date"])
|
||||
|
||||
for service in data["object_list"]:
|
||||
service.stats = service.get_core_stats(
|
||||
self.get_start_date(), self.get_end_date()
|
||||
)
|
||||
|
||||
return data
|
||||
|
||||
|
||||
@@ -58,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.6.6"
|
||||
VERSION = "v0.10.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__)))
|
||||
@@ -58,6 +58,7 @@ INSTALLED_APPS = [
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"debug_toolbar",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@@ -70,6 +71,7 @@ MIDDLEWARE = [
|
||||
"django.contrib.sites.middleware.CurrentSiteMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
||||
]
|
||||
|
||||
ROOT_URLCONF = "shynet.urls"
|
||||
@@ -145,9 +147,15 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
|
||||
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
|
||||
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
# Logging
|
||||
@@ -247,6 +255,10 @@ LOGIN_REDIRECT_URL = "/"
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
INTERNAL_IPS = [
|
||||
"127.0.0.1",
|
||||
]
|
||||
|
||||
# Celery
|
||||
|
||||
CELERY_TASK_ALWAYS_EAGER = os.getenv("CELERY_TASK_ALWAYS_EAGER", "True") == "True"
|
||||
@@ -289,11 +301,22 @@ 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")],
|
||||
"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
|
||||
@@ -318,3 +341,25 @@ SESSION_MEMORY_TIMEOUT = int(os.getenv("SESSION_MEMORY_TIMEOUT", "1800"))
|
||||
|
||||
# Should the Shynet version information be displayed?
|
||||
SHOW_SHYNET_VERSION = os.getenv("SHOW_SHYNET_VERSION", "True") == "True"
|
||||
|
||||
# Should Shynet show third-party icons in the dashboard?
|
||||
SHOW_THIRD_PARTY_ICONS = os.getenv("SHOW_THIRD_PARTY_ICONS", "True") == "True"
|
||||
|
||||
# Should Shynet never collect any IP?
|
||||
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"))
|
||||
|
||||
# Should background bars be scaled to full width?
|
||||
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION = (
|
||||
os.getenv("USE_RELATIVE_MAX_IN_BAR_VISUALIZATION", "True") == "True"
|
||||
)
|
||||
|
||||
@@ -15,8 +15,10 @@ Including another URLconf
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
import debug_toolbar
|
||||
|
||||
urlpatterns = [
|
||||
path("__debug__/", include(debug_toolbar.urls)),
|
||||
path("admin/", admin.site.urls),
|
||||
path("accounts/", include("allauth.urls")),
|
||||
path("ingress/", include(("analytics.ingress_urls", "ingress")), name="ingress"),
|
||||
|
||||
@@ -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