Compare commits

...

137 Commits

Author SHA1 Message Date
R. Miles McCain
ff97a46fd9 Bump version 2021-05-14 16:33:11 +00:00
R. Miles McCain
afb78dc499 Merge branch 'pagination-settings' 2021-05-14 16:25:22 +00:00
R. Miles McCain
a4eaef0117 Merge branch 'hourly-chart' 2021-05-14 16:25:06 +00:00
R. Miles McCain
7891866214 Merge branch 'skip_heartbeat' 2021-05-14 16:24:47 +00:00
R. Miles McCain
eedcbc4e85 Merge branch 'haaavk/master' 2021-05-14 16:24:20 +00:00
R. Miles McCain
0d006620dd Merge branch 'custom-map-provider' 2021-05-14 16:23:56 +00:00
R. Miles McCain
0b78f6df72 Merge branch 'ui-improvements' 2021-05-14 16:22:31 +00:00
R. Miles McCain
74ddef1670 Merge branch 'flag-icons' 2021-05-14 16:21:34 +00:00
R. Miles McCain
9d9d4a7b1e Bump version to v0.9.0 2021-05-14 16:16:12 +00:00
Paweł Jastrzębski
d66f683104 Add DASHBOARD_PAGE_SIZE to settings
Add DASHBOARD_PAGE_SIZE to TEMPLATE.env and app.json

Parse dashboard page size value
2021-05-14 16:09:45 +00:00
CasperVerswijvelt
b44642e023 When daterange is 1 day, show hourly data in chart
Show hourly chart starting from ranges of 3 days and less

Use timedelta instead of checking days difference which did not work correctly

Fix hourly chart bug

Add click handler for going from daily to hourly chart view by clicking chart at specific date
2021-05-14 16:09:45 +00:00
R. Miles McCain
c12a7e9e71 Document ACTIVE_USER_TIMEDELTA 2021-05-14 16:09:45 +00:00
R. Miles McCain
0294d31ea4 Remove console warning from script 2021-05-14 16:09:45 +00:00
Paweł Jastrzębski
40cb5afbad Skip heartbeat if there is no response
Fix xhr callbacks
2021-05-14 16:09:45 +00:00
Paweł Jastrzębski
073bd94112 Update litepicker and add ranges plugin
Fix litepicker colors

Fix litepicker event

Add custom ranges to litepicker

Fix code style

Remove some date ranges

Fix date ranges

Replace yesterday date range with last 3 days

Update packages

Improve litepicker box shadow
2021-05-14 16:09:45 +00:00
CasperVerswijvelt
3a01fefcff Add custom location url from environment variable
Remove trailing dollar in long and lat placeholder
2021-05-14 16:09:45 +00:00
CasperVerswijvelt
14a7ec68f3 Make favicon not squish and add ellipsis overflow
General styling improvements

Many UI Improvements

- Consistent spacing between titles and content
- Removed many ugly text squishing by hiding overflowing text with ellipsis
- Fixed Service favicon being squisched by long service name
- Hide scrollbar in 'more session' screen when content isn't scrollable
- Fix apexcharts tooltips and labels being cut off by card class

Disable wrapping in table cells, prefer ellipsis

Ellipsis overflow for long url on hit page

Fix flex grow in header not working as intended

Remove forgotten truncatechars

Fix code checks, add button role and tabindex
2021-05-14 16:09:45 +00:00
CasperVerswijvelt
fdf2ab719b Use flag icons instead of emoji's 2021-05-14 16:09:45 +00:00
R. Miles McCain
737eeb5df4 Contextual date improvements 2021-05-14 16:09:45 +00:00
CasperVerswijvelt
cb4855e4fc Preserve date range in urls in side menu 2021-05-14 16:09:45 +00:00
CasperVerswijvelt
f4127cf9b1 Preserve date range query parameters 2021-05-14 16:09:45 +00:00
R. Miles McCain
159015de1c Update test for my new instance 2021-05-14 16:09:45 +00:00
Paweł Jastrzębski
a7548d7eba Fix currently_online
Make currently_online aware of SCRIPT_HEARTBEAT_FREQUENCY
2021-05-14 16:09:45 +00:00
R. Miles McCain
da87ddb18f Fix ingest when MMDB not found 2021-05-14 16:09:45 +00:00
R. Miles McCain
4a76ab32fc Improve service page when no hits are recorded 2021-05-14 16:09:45 +00:00
R. Miles McCain
4afeced7d3 Improve homepage when no services exist 2021-05-14 16:09:45 +00:00
CasperVerswijvelt
2a6efe1b7f Merge 2 steps 2021-05-14 16:09:44 +00:00
CasperVerswijvelt
07f3926a9c Show snippet on service page when not hits are recorded yet 2021-05-14 16:09:44 +00:00
R. Miles McCain
14ed0b7979 Merge branch 'docker-github-build' 2021-05-14 16:06:37 +00:00
CasperVerswijvelt
ab51089647 Fix code checks, add button role and tabindex 2021-05-14 15:01:58 +00:00
CasperVerswijvelt
86695dbcc4 Remove forgotten truncatechars 2021-05-14 15:01:57 +00:00
CasperVerswijvelt
4e4cfe081b Fix flex grow in header not working as intended 2021-05-14 15:01:31 +00:00
CasperVerswijvelt
f54b67ef0f Ellipsis overflow for long url on hit page 2021-05-14 15:01:11 +00:00
CasperVerswijvelt
43f339e32b Disable wrapping in table cells, prefer ellipsis 2021-05-14 14:59:38 +00:00
CasperVerswijvelt
b144efaa9b Many UI Improvements
- Consistent spacing between titles and content
- Removed many ugly text squishing by hiding overflowing text with ellipsis
- Fixed Service favicon being squisched by long service name
- Hide scrollbar in 'more session' screen when content isn't scrollable
- Fix apexcharts tooltips and labels being cut off by card class
2021-05-14 14:58:00 +00:00
CasperVerswijvelt
c06b7a094a General styling improvements 2021-05-14 14:54:44 +00:00
CasperVerswijvelt
f13745f15e Make favicon not squish and add ellipsis overflow 2021-05-14 14:54:42 +00:00
CasperVerswijvelt
5c782ddb7d Use flag icons instead of emoji's 2021-05-14 14:52:11 +00:00
R. Miles McCain
a6a508899a Contextual date improvements 2021-05-14 14:50:04 +00:00
CasperVerswijvelt
2b003b8fa9 Preserve date range in urls in side menu 2021-05-08 12:27:32 +02:00
CasperVerswijvelt
023e0fde15 Preserve date range query parameters 2021-05-06 21:23:05 +02:00
Paweł Jastrzębski
a1a083a403 Replace yesterday date range with last 3 days 2021-05-05 14:56:17 +02:00
Paweł Jastrzębski
8b167b2c74 Fix date ranges 2021-05-05 14:54:33 +02:00
CasperVerswijvelt
4cd0c4735d Add click handler for going from daily to hourly chart view by clicking chart at specific date 2021-05-01 22:04:38 +02:00
Paweł Jastrzębski
d9e1ffddb1 Add DASHBOARD_PAGE_SIZE to TEMPLATE.env and app.json 2021-04-30 14:01:12 +02:00
Paweł Jastrzębski
9fb875f749 Add DASHBOARD_PAGE_SIZE to settings 2021-04-30 08:36:50 +02:00
CasperVerswijvelt
f6e502dfbd Remove trailing dollar in long and lat placeholder 2021-04-28 21:50:05 +02:00
CasperVerswijvelt
7c69b0bd81 Fix hourly chart bug 2021-04-28 18:08:26 +02:00
CasperVerswijvelt
78bea501a8 Use timedelta instead of checking days difference which did not work correctly 2021-04-27 23:39:58 +02:00
CasperVerswijvelt
c2daf3a5a5 Show hourly chart starting from ranges of 3 days and less 2021-04-27 21:58:07 +02:00
CasperVerswijvelt
df6786e037 Change docker push tags 2021-04-26 18:21:31 +02:00
Casper Verswijvelt
6621625d90 Multi arch docker build 2021-04-26 17:36:38 +02:00
CasperVerswijvelt
32ae0aa5f3 Change locationUrl to snake casing 2021-04-25 22:55:17 +02:00
CasperVerswijvelt
2221a99662 When daterange is 1 day, show hourly data in chart 2021-04-25 22:54:06 +02:00
Paweł Jastrzębski
69ec37331a Fix xhr callbacks 2021-04-25 17:32:23 +02:00
R. Miles McCain
03f88af03c Update test for my new instance 2021-04-24 17:35:55 +00:00
R. Miles McCain
87b7ce2edc Merge branch 'improve_currently_online' into dev 2021-04-24 17:27:27 +00:00
R. Miles McCain
26c1ae2bce Fix ingest when MMDB not found 2021-04-24 17:21:29 +00:00
R. Miles McCain
36d72508e6 Merge branch 'CasperVerswijvelt/master' into dev 2021-04-24 17:07:24 +00:00
R. Miles McCain
68945df17d Improve service page when no hits are recorded 2021-04-24 17:06:32 +00:00
R. Miles McCain
fef531efa9 Improve homepage when no services exist 2021-04-24 17:06:19 +00:00
CasperVerswijvelt
46176b19fc Merge 2 steps 2021-04-24 13:04:21 +02:00
CasperVerswijvelt
94c53d2ab5 Show snippet on service page when not hits are recorded yet 2021-04-24 13:01:03 +02:00
Paweł Jastrzębski
ea893b2322 Skip heartbeat if there is no response 2021-04-23 19:28:31 +02:00
Paweł Jastrzębski
71431fcaa6 Fix currently_online
Make currently_online aware of SCRIPT_HEARTBEAT_FREQUENCY
2021-04-23 11:00:32 +02:00
Paweł Jastrzębski
e9536f1816 Remove some date ranges 2021-04-22 19:58:13 +02:00
Paweł Jastrzębski
6f835a4f27 Fix code style 2021-04-22 17:04:06 +02:00
Paweł Jastrzębski
faf4f48e75 Add custom ranges to litepicker 2021-04-22 15:57:19 +02:00
CasperVerswijvelt
278306daa4 Add custom location url from environment variable 2021-04-21 20:16:28 +02:00
Paweł Jastrzębski
2c0fafefea Fix litepicker event 2021-04-21 18:30:33 +02:00
Paweł Jastrzębski
6eb41e016a Fix litepicker colors 2021-04-21 18:13:33 +02:00
CasperVerswijvelt
369f4d8d6b Use flag icons instead of emoji's 2021-04-21 16:14:26 +02:00
Paweł Jastrzębski
3d43f223eb Update litepicker and add ranges plugin 2021-04-21 11:47:50 +02:00
dependabot[bot]
351efff147 Bump django from 3.1.7 to 3.1.8 (#115)
Bumps [django](https://github.com/django/django) from 3.1.7 to 3.1.8.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.7...3.1.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-16 17:02:07 -04:00
dependabot[bot]
6867cbd282 Bump django-debug-toolbar from 3.2 to 3.2.1 (#117)
Bumps [django-debug-toolbar](https://github.com/jazzband/django-debug-toolbar) from 3.2 to 3.2.1.
- [Release notes](https://github.com/jazzband/django-debug-toolbar/releases)
- [Changelog](https://github.com/jazzband/django-debug-toolbar/blob/main/docs/changes.rst)
- [Commits](https://github.com/jazzband/django-debug-toolbar/compare/3.2...3.2.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-04-16 17:01:58 -04:00
CasperVerswijvelt
c03ef52ba8 Fix pixel request from not allowed origin triggering a hit 2021-04-02 21:21:24 +02:00
R. Miles McCain
9cb030ecbd Bump version 2021-03-29 15:09:27 +00:00
R. Miles McCain
8bab14cc8a Separate bounce migration into two 2021-03-29 15:04:36 +00:00
R. Miles McCain
fe8e766670 Format 2021-03-29 14:37:59 +00:00
R. Miles McCain
b63863e283 Bump version 2021-03-29 14:37:21 +00:00
R. Miles McCain
516f9fb951 Fix aggressive hash salting 2021-03-29 14:37:08 +00:00
R. Miles McCain
c2234ec647 Bump version 2021-03-28 22:15:30 +00:00
R. Miles McCain
02cbee5c8c Cache bounce 2021-03-28 21:55:38 +00:00
R. Miles McCain
518436ffd2 Relock npm packages 2021-03-28 21:37:09 +00:00
R. Miles McCain
311aa2b1ac Drop Turbolinks 2021-03-28 21:36:53 +00:00
R. Miles McCain
8ad44ddc23 Add pagination to dashboard 2021-03-28 21:29:54 +00:00
R. Miles McCain
874aad87a8 Store service directly in Hit 2021-03-28 20:54:19 +00:00
R. Miles McCain
f2e875d03d Add indexes to key Hit fields 2021-03-28 19:18:57 +00:00
R. Miles McCain
45fd32c8ca Index last_seen 2021-03-28 19:15:03 +00:00
R. Miles McCain
08b36ba69f Integrate debug toolbar 2021-03-28 19:14:56 +00:00
R. Miles McCain
d5cfe577a0 Add debug toolbar 2021-03-28 19:14:33 +00:00
R. Miles McCain
c131cfef27 Merge branch 'patch-1' into dev 2021-03-28 18:54:24 +00:00
R. Miles McCain
526d4cd133 Relock Pipfile 2021-03-28 18:53:49 +00:00
R. Miles McCain
8e09871b44 Merge branch 'dependabot/pip/django-3.1.6' into dev 2021-03-28 18:51:33 +00:00
R. Miles McCain
6aa3ce0b32 Merge branch 'dependabot/pip/pyyaml-5.4' into dev 2021-03-28 18:49:49 +00:00
dependabot[bot]
23ea8e493e Bump pyyaml from 5.3.1 to 5.4
Bumps [pyyaml](https://github.com/yaml/pyyaml) from 5.3.1 to 5.4.
- [Release notes](https://github.com/yaml/pyyaml/releases)
- [Changelog](https://github.com/yaml/pyyaml/blob/master/CHANGES)
- [Commits](https://github.com/yaml/pyyaml/compare/5.3.1...5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-25 23:41:11 +00:00
dependabot[bot]
22d996bed7 Bump django from 3.1.3 to 3.1.6
Bumps [django](https://github.com/django/django) from 3.1.3 to 3.1.6.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.3...3.1.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-19 02:04:53 +00:00
Kasper Seweryn
9df864787c Fix #99 2021-02-17 02:24:35 +00:00
dependabot[bot]
b7a6ac9ec0 Bump apexcharts from 3.23.1 to 3.24.0 (#97)
Bumps [apexcharts](https://github.com/apexcharts/apexcharts.js) from 3.23.1 to 3.24.0.
- [Release notes](https://github.com/apexcharts/apexcharts.js/releases)
- [Commits](https://github.com/apexcharts/apexcharts.js/commits)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-02-15 18:56:29 -05:00
R. Miles McCain
38d8d416e1 Update kubernetes deployments 2021-01-25 13:39:53 -05:00
R. Miles McCain
592613a99a Use dev shynet in kubernetes deployments 2021-01-23 23:28:24 -05:00
R. Miles McCain
e9f43c6a53 Bump version 2021-01-23 23:23:19 -05:00
R. Miles McCain
89c6800913 Fix formatting 2021-01-23 23:16:33 -05:00
R. Miles McCain
db9c807289 Add optional more aggressive salting (fixes #95) 2021-01-23 23:13:44 -05:00
R. Miles McCain
6e48a3eac7 Merge branch 'global-ip-block' into dev 2021-01-23 23:01:53 -05:00
R. Miles McCain
ba9a716913 Merge branch 'heartbeat-frequency' into dev 2021-01-23 22:41:25 -05:00
R. Miles McCain
6d7292a60a Fix duration change being unknown (fixes #89) 2021-01-23 22:40:19 -05:00
Oliver Kamer
c0d02732e7 Add additional env variable to template 2021-01-19 22:05:33 +01:00
Oliver Kamer
d071a91917 Block Collect IP option if disabled globally 2021-01-19 22:02:57 +01:00
Oliver Kamer
d67e14b08f Block IP collection from settings 2021-01-19 21:41:54 +01:00
Oliver Kamer
174a386f54 Add block all ips to settings 2021-01-19 21:31:02 +01:00
Oliver Kamer
ce23cfc5b5 Add pycharm gitignore stuff 2021-01-19 21:20:30 +01:00
Oliver Kamer
8be690c417 Use heartbeat frequency for currently active
If the heartbeat frequency is more than 10 seconds, shynet will display as not active, even though it still is.

Using 2x the heartbeat frequency should give better results.
2021-01-19 11:32:36 +01:00
R. Miles McCain
2f778dc4b4 Bump version to v0.7.3 2021-01-11 12:12:15 -05:00
R. Miles McCain
e0c165313b Add fallback to percent_change_display (fixes #89) 2021-01-11 12:11:27 -05:00
R. Miles McCain
c86192d301 Improve a17t colors 2021-01-10 12:25:08 -05:00
R. Miles McCain
775c105d1d Bump version to 0.7.2 2021-01-10 12:20:17 -05:00
R. Miles McCain
be85c0a560 Update and trim dependencies 2021-01-10 12:19:54 -05:00
R. Miles McCain
70e1af15cc Fix division by zero error 2021-01-10 12:17:53 -05:00
R. Miles McCain
6afea91c5f Bump version 2020-11-26 21:03:52 +00:00
R. Miles McCain
7a4c892804 Remove background from favicon 2020-11-26 21:03:18 +00:00
imgbot[bot]
9b50b1ea42 [ImgBot] Optimize images (#87)
*Total -- 765.10kb -> 502.59kb (34.31%)

/images/service.png -- 366.95kb -> 239.64kb (34.69%)
/images/homepage.png -- 398.15kb -> 262.95kb (33.96%)

Signed-off-by: ImgBotApp <ImgBotHelp@gmail.com>

Co-authored-by: ImgBotApp <ImgBotHelp@gmail.com>
2020-11-26 16:00:51 -05:00
R. Miles McCain
52a18d21f1 Small visual improvements 2020-11-26 20:59:03 +00:00
R. Miles McCain
8aaf312c67 Bump version 2020-11-26 20:10:28 +00:00
R. Miles McCain
ede06900e5 Format code 2020-11-26 20:09:55 +00:00
R. Miles McCain
a42455c9dc Add browser icon 2020-11-26 20:09:34 +00:00
R. Miles McCain
547a84f2fc Update screenshots 2020-11-26 20:04:48 +00:00
R. Miles McCain
f56ea99dc2 Add demo data command 2020-11-26 19:44:07 +00:00
R. Miles McCain
4cea5d2310 Add additional icons 2020-11-26 19:43:44 +00:00
R. Miles McCain
e4f09b4e68 Ensure times are always correct 2020-11-26 19:43:25 +00:00
R. Miles McCain
cc094fe04e Add icons to dashboard 2020-11-26 18:09:42 +00:00
R. Miles McCain
ac5c743390 Update dependencies 2020-11-26 18:00:19 +00:00
R. Miles McCain
963db18642 Bump version 2020-10-17 19:11:25 +00:00
R. Miles McCain
748fb76eaf Bump version 2020-10-17 19:03:37 +00:00
R. Miles McCain
d93a698e87 Update django-redis-cache 2020-10-17 19:03:18 +00:00
R. Miles McCain
ca9ee2f1f5 Bump version 2020-10-17 18:54:22 +00:00
R. Miles McCain
9146c889ac Show Shynet version when envvar is true 2020-10-17 18:53:46 +00:00
57 changed files with 1221 additions and 286 deletions

46
.github/workflows/build-docker.yml vendored Normal file
View File

@@ -0,0 +1,46 @@
name: Build docker images
on:
push:
tags:
- "*"
jobs:
publish_to_docker_hub:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Prepare tags
id: prep
run: |
DOCKER_IMAGE=milesmcc/shynet
VERSION=${GITHUB_REF#refs/tags/}
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:latest"
echo ::set-output name=tags::${TAGS}
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v1
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Build and push advanced image
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.prep.outputs.tags }}

5
.gitignore vendored
View File

@@ -138,4 +138,7 @@ dmypy.json
secrets.yml secrets.yml
.vscode .vscode
.DS_Store .DS_Store
compiledstatic/ compiledstatic/
# Pycharm
.idea

View File

@@ -1,4 +1,4 @@
FROM python:3-alpine FROM python:alpine3.12
# Getting things ready # Getting things ready
WORKDIR /usr/src/shynet WORKDIR /usr/src/shynet

10
Pipfile
View File

@@ -4,22 +4,22 @@ url = "https://pypi.org/simple"
verify_ssl = true verify_ssl = true
[packages] [packages]
django = "~=3.0" django = "~=3.1"
django-allauth = "~=0.42.0" django-allauth = "~=0.42.0"
geoip2 = "~=3.0.0" geoip2 = "~=3.0.0"
whitenoise = "~=5.1.0" whitenoise = "~=5.1.0"
celery = "~=4.4.6" celery = "~=4.4.6"
django-ipware = "~=2.1.0" django-ipware = "~=2.1.0"
pyyaml = "~=5.3.1" pyyaml = "~=5.4"
ua-parser = "~=0.10.0" ua-parser = "~=0.10.0"
user-agents = "~=2.1" user-agents = "~=2.1"
emoji-country-flag = "~=1.2.1"
rules = "~=2.2" rules = "~=2.2"
gunicorn = "~=20.0.4" gunicorn = "~=20.0.4"
psycopg2-binary = "~=2.8.5" psycopg2-binary = "~=2.8.5"
redis = "~=3.5.3" redis = "~=3.5.3"
django-redis-cache = "~=2.1.1" django-redis-cache = "~=3.0.0"
pycountry = "~=19.8.18" pycountry = "~=19.8.18"
html2text = "~=2020.1.16" html2text = "~=2020.1.16"
django-health-check = "~=3.12.1" django-health-check = "~=3.12.1"
django-npm = "~=1.0.0" django-npm = "~=1.0.0"
django-debug-toolbar = "*"

128
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "f9ab7bf92fe2342e3d221ab34ac957bf217638062da0554b2328502650515741" "sha256": "c51ea0205c9ffe753b9ef5249cd49c2338bb50768ae104113bfb7b97b5f9d70c"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": {}, "requires": {},
@@ -23,17 +23,17 @@
}, },
"asgiref": { "asgiref": {
"hashes": [ "hashes": [
"sha256:7e51911ee147dd685c3c8b805c0ad0cb58d360987b56953878f8c06d2d1c6f1a", "sha256:92906c611ce6c967347bbfea733f13d6313901d54dcca88195eaeb52b2a8e8ee",
"sha256:9fc6fb5d39b8af147ba40765234fa822b39818b12cc80b35ad9b0cef3a476aed" "sha256:d1216dfbdfb63826470995d31caed36225dcaf34f182e0fa257a4dd9e86f1b78"
], ],
"version": "==3.2.10" "version": "==3.3.4"
}, },
"billiard": { "billiard": {
"hashes": [ "hashes": [
"sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede", "sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547",
"sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a" "sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"
], ],
"version": "==3.6.3.0" "version": "==3.6.4.0"
}, },
"celery": { "celery": {
"hashes": [ "hashes": [
@@ -45,32 +45,32 @@
}, },
"certifi": { "certifi": {
"hashes": [ "hashes": [
"sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3", "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c",
"sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41" "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"
], ],
"version": "==2020.6.20" "version": "==2020.12.5"
}, },
"chardet": { "chardet": {
"hashes": [ "hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
], ],
"version": "==3.0.4" "version": "==4.0.0"
}, },
"defusedxml": { "defusedxml": {
"hashes": [ "hashes": [
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69",
"sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5" "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"
], ],
"version": "==0.6.0" "version": "==0.7.1"
}, },
"django": { "django": {
"hashes": [ "hashes": [
"sha256:a2127ad0150ec6966655bedf15dbbff9697cc86d61653db2da1afa506c0b04cc", "sha256:c348b3ddc452bf4b62361f0752f71a339140c777ebea3cdaaaa8fdb7f417a862",
"sha256:c93c28ccf1d094cbd00d860e83128a39e45d2c571d3b54361713aaaf9a94cac4" "sha256:f8393103e15ec2d2d313ccbb95a3f1da092f9f58d74ac1c61ca2ac0436ae1eac"
], ],
"index": "pypi", "index": "pypi",
"version": "==3.1.2" "version": "==3.1.8"
}, },
"django-allauth": { "django-allauth": {
"hashes": [ "hashes": [
@@ -79,6 +79,14 @@
"index": "pypi", "index": "pypi",
"version": "==0.42.0" "version": "==0.42.0"
}, },
"django-debug-toolbar": {
"hashes": [
"sha256:a5ff2a54f24bf88286f9872836081078f4baa843dc3735ee88524e89f8821e33",
"sha256:e759e63e3fe2d3110e0e519639c166816368701eab4a47fed75d7de7018467b9"
],
"index": "pypi",
"version": "==3.2.1"
},
"django-health-check": { "django-health-check": {
"hashes": [ "hashes": [
"sha256:2667b89b8f85ad9b2a24c90581b376016d22ea912fedf37f9866413a3c2e0a5d", "sha256:2667b89b8f85ad9b2a24c90581b376016d22ea912fedf37f9866413a3c2e0a5d",
@@ -103,19 +111,10 @@
}, },
"django-redis-cache": { "django-redis-cache": {
"hashes": [ "hashes": [
"sha256:9b2c45a1bc0f295bccd56c2542d937665ae98f3325f20b3d82fc620e14395d52", "sha256:9a2eebef421d996a82098a19d17ff6b321265cd73178fa398913019764e8394a"
"sha256:e72691539be99c0b2dd64ac380e26f4d2be5c53c1b2d26845dd279ae38b47477"
], ],
"index": "pypi", "index": "pypi",
"version": "==2.1.3" "version": "==3.0.0"
},
"emoji-country-flag": {
"hashes": [
"sha256:67c0cb6a3765fb53f31b34160d6b1c8a5f44b297bc278d1835c6f2e5b0a9a592",
"sha256:ae7edb38077b0840210fa9e37673f481f2b9c032446e13ad6dab2b1108cd7ad6"
],
"index": "pypi",
"version": "==1.2.1"
}, },
"geoip2": { "geoip2": {
"hashes": [ "hashes": [
@@ -173,9 +172,11 @@
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c", "sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
"sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67", "sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
"sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0", "sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
"sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6",
"sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db", "sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
"sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94", "sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
"sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52", "sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
"sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056",
"sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b", "sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
"sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd", "sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
"sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550", "sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
@@ -223,27 +224,45 @@
}, },
"pytz": { "pytz": {
"hashes": [ "hashes": [
"sha256:a494d53b6d39c3c6e44c3bec237336e14305e4f29bbf800b599253057fbb79ed", "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da",
"sha256:c35965d010ce31b23eeb663ed3cc8c906275d6be1a34393a1d73a41febf4a048" "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"
], ],
"version": "==2020.1" "version": "==2021.1"
}, },
"pyyaml": { "pyyaml": {
"hashes": [ "hashes": [
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", "sha256:08682f6b72c722394747bddaf0aa62277e02557c0fd1c42cb853016a38f8dedf",
"sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76", "sha256:0f5f5786c0e09baddcd8b4b45f20a7b5d61a7e7e99846e3c799b05c7c53fa696",
"sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2", "sha256:129def1b7c1bf22faffd67b8f3724645203b79d8f4cc81f674654d9902cb4393",
"sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648", "sha256:294db365efa064d00b8d1ef65d8ea2c3426ac366c0c4368d930bf1c5fb497f77",
"sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf", "sha256:3b2b1824fe7112845700f815ff6a489360226a5609b96ec2190a45e62a9fc922",
"sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f", "sha256:3bd0e463264cf257d1ffd2e40223b197271046d09dadf73a0fe82b9c1fc385a5",
"sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2", "sha256:4465124ef1b18d9ace298060f4eccc64b0850899ac4ac53294547536533800c8",
"sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee", "sha256:49d4cdd9065b9b6e206d0595fee27a96b5dd22618e7520c33204a4a3239d5b10",
"sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d", "sha256:4e0583d24c881e14342eaf4ec5fbc97f934b999a6828693a99157fde912540cc",
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c", "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018",
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" "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", "index": "pypi",
"version": "==5.3.1" "version": "==5.4.1"
}, },
"redis": { "redis": {
"hashes": [ "hashes": [
@@ -255,10 +274,10 @@
}, },
"requests": { "requests": {
"hashes": [ "hashes": [
"sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b", "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
"sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898" "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
], ],
"version": "==2.24.0" "version": "==2.25.1"
}, },
"requests-oauthlib": { "requests-oauthlib": {
"hashes": [ "hashes": [
@@ -274,13 +293,6 @@
"index": "pypi", "index": "pypi",
"version": "==2.2" "version": "==2.2"
}, },
"six": {
"hashes": [
"sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
"sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
],
"version": "==1.15.0"
},
"sqlparse": { "sqlparse": {
"hashes": [ "hashes": [
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0", "sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
@@ -298,10 +310,10 @@
}, },
"urllib3": { "urllib3": {
"hashes": [ "hashes": [
"sha256:91056c15fa70756691db97756772bb1eb9678fa585d9184f24534b100dc60f4a", "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df",
"sha256:e7983572181f5e1522d9c98453462384ee92a0be7fac5f1413a1e35c56cc0461" "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"
], ],
"version": "==1.25.10" "version": "==1.26.4"
}, },
"user-agents": { "user-agents": {
"hashes": [ "hashes": [

View File

@@ -70,3 +70,29 @@ SHOW_SHYNET_VERSION=True
# that you have a separate queue consumer running somewhere via `celeryworker.sh`. # that you have a separate queue consumer running somewhere via `celeryworker.sh`.
# CELERY_TASK_ALWAYS_EAGER=False # CELERY_TASK_ALWAYS_EAGER=False
# CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1 # 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

View File

@@ -122,6 +122,16 @@
"description": "Set to 'False' if you do not want the version to be displayed on the frontend.", "description": "Set to 'False' if you do not want the version to be displayed on the frontend.",
"value": "True", "value": "True",
"required": false "required": false
},
"LOCATION_URL": {
"description": "Custom location url to link to in frontend.",
"value": "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE",
"required": false
},
"DASHBOARD_PAGE_SIZE": {
"description": "How many services should be displayed on dashboard page?",
"value": "5",
"required": false
} }
} }
} }

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

View File

@@ -17,7 +17,7 @@ spec:
spec: spec:
containers: containers:
- name: "shynet-webserver" - name: "shynet-webserver"
image: "milesmcc/shynet:latest" image: "milesmcc/shynet:dev"
imagePullPolicy: Always imagePullPolicy: Always
envFrom: envFrom:
- secretRef: - secretRef:
@@ -42,7 +42,7 @@ spec:
spec: spec:
containers: containers:
- name: "shynet-celeryworker" - name: "shynet-celeryworker"
image: "milesmcc/shynet:latest" image: "milesmcc/shynet:dev"
command: ["./celeryworker.sh"] command: ["./celeryworker.sh"]
imagePullPolicy: Always imagePullPolicy: Always
envFrom: envFrom:
@@ -61,7 +61,7 @@ spec:
selector: selector:
app: shynet-redis app: shynet-redis
--- ---
apiVersion: apps/v1beta2 apiVersion: apps/v1
kind: StatefulSet kind: StatefulSet
metadata: metadata:
name: shynet-redis name: shynet-redis
@@ -83,3 +83,37 @@ spec:
ports: ports:
- containerPort: 6379 - containerPort: 6379
name: redis 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: /

183
package-lock.json generated
View File

@@ -1,22 +1,163 @@
{ {
"name": "shynet", "name": "shynet",
"lockfileVersion": 2,
"requires": true, "requires": true,
"lockfileVersion": 1, "packages": {
"": {
"license": "Apache-2.0",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"a17t": "^0.5.1",
"apexcharts": "^3.24.0",
"flag-icon-css": "^3.5.0",
"inter-ui": "^3.15.0",
"litepicker": "^2.0.11"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "5.15.3",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
"integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w==",
"hasInstallScript": true,
"engines": {
"node": ">=6"
}
},
"node_modules/a17t": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/a17t/-/a17t-0.5.1.tgz",
"integrity": "sha512-peIPrH9eDiu49LLzLlSTFFrXj6WLlEX3TRsUkqyyOHi/i58ilJ/eERnu7AcswXhuCBx+/2W9EUuHM+8iAq4ipg=="
},
"node_modules/apexcharts": {
"version": "3.26.2",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.26.2.tgz",
"integrity": "sha512-CD7bad4ygwc9rs9vOQDDagUcoJ1mcc9BwNSiQB14l6jiZBCQKrXxnG4I1ZjJ2MIel/Y5GmsJFs8HTcZBqpe/Ew==",
"dependencies": {
"svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0",
"svg.filter.js": "^2.0.2",
"svg.pathmorphing.js": "^0.1.3",
"svg.resize.js": "^1.4.3",
"svg.select.js": "^3.0.1"
},
"funding": {
"url": "https://github.com/apexcharts/apexcharts.js?sponsor=1"
}
},
"node_modules/flag-icon-css": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/flag-icon-css/-/flag-icon-css-3.5.0.tgz",
"integrity": "sha512-pgJnJLrtb0tcDgU1fzGaQXmR8h++nXvILJ+r5SmOXaaL/2pocunQo2a8TAXhjQnBpRLPtZ1KCz/TYpqeNuE2ew=="
},
"node_modules/inter-ui": {
"version": "3.18.1",
"resolved": "https://registry.npmjs.org/inter-ui/-/inter-ui-3.18.1.tgz",
"integrity": "sha512-W3LnAirp6a1ixpAHZwr9gH52KlOQOAp0oqbmIoGi2dAIlcIB7auJgLr9XFHUzYy2FoZ0Nf7aPe/nHMZB4/Zvdg=="
},
"node_modules/litepicker": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/litepicker/-/litepicker-2.0.11.tgz",
"integrity": "sha512-7MECMp2EDGIYDIz9QT24t9hWpgBD9JD57ZdDrbffNMGfbw0JVhBhvlYsyaIUuYhywtLvgmI5lfulM7XF2HLEkg=="
},
"node_modules/svg.draggable.js": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz",
"integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==",
"dependencies": {
"svg.js": "^2.0.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.easing.js": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz",
"integrity": "sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=",
"dependencies": {
"svg.js": ">=2.3.x"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.filter.js": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz",
"integrity": "sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=",
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz",
"integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA=="
},
"node_modules/svg.pathmorphing.js": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz",
"integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==",
"dependencies": {
"svg.js": "^2.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz",
"integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==",
"dependencies": {
"svg.js": "^2.6.5",
"svg.select.js": "^2.1.2"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.resize.js/node_modules/svg.select.js": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz",
"integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==",
"dependencies": {
"svg.js": "^2.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/svg.select.js": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz",
"integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==",
"dependencies": {
"svg.js": "^2.6.5"
},
"engines": {
"node": ">= 0.8.0"
}
}
},
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": { "@fortawesome/fontawesome-free": {
"version": "5.15.1", "version": "5.15.3",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.1.tgz", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.3.tgz",
"integrity": "sha512-OEdH7SyC1suTdhBGW91/zBfR6qaIhThbcN8PUXtXilY4GYnSBbVqOntdHbC1vXwsDnX0Qix2m2+DSU1J51ybOQ==" "integrity": "sha512-rFnSUN/QOtnOAgqFRooTA3H57JLDm0QEG/jPdk+tLQNL/eWd+Aok8g3qCI+Q1xuDPWpGW/i9JySpJVsq8Q0s9w=="
}, },
"a17t": { "a17t": {
"version": "0.2.9", "version": "0.5.1",
"resolved": "https://registry.npmjs.org/a17t/-/a17t-0.2.9.tgz", "resolved": "https://registry.npmjs.org/a17t/-/a17t-0.5.1.tgz",
"integrity": "sha512-PdkeIVJYTfKCrYkx64c8HEPvbiVo2Prx8NWMCsiXHbsvPLbai64FwydXnNSzq/hRBQ3Toi5qU8DNCxeX1AXBCw==" "integrity": "sha512-peIPrH9eDiu49LLzLlSTFFrXj6WLlEX3TRsUkqyyOHi/i58ilJ/eERnu7AcswXhuCBx+/2W9EUuHM+8iAq4ipg=="
}, },
"apexcharts": { "apexcharts": {
"version": "3.22.0", "version": "3.26.2",
"resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.22.0.tgz", "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.26.2.tgz",
"integrity": "sha512-DDh2eXnAEA8GoKU/hdicOaS2jzGehXwv8Bj1djYYudkeQzEdglFoWsVyIxff+Ds7+aUtVAJzd/9ythZuyyIbXQ==", "integrity": "sha512-CD7bad4ygwc9rs9vOQDDagUcoJ1mcc9BwNSiQB14l6jiZBCQKrXxnG4I1ZjJ2MIel/Y5GmsJFs8HTcZBqpe/Ew==",
"requires": { "requires": {
"svg.draggable.js": "^2.2.2", "svg.draggable.js": "^2.2.2",
"svg.easing.js": "^2.0.0", "svg.easing.js": "^2.0.0",
@@ -26,15 +167,20 @@
"svg.select.js": "^3.0.1" "svg.select.js": "^3.0.1"
} }
}, },
"flag-icon-css": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/flag-icon-css/-/flag-icon-css-3.5.0.tgz",
"integrity": "sha512-pgJnJLrtb0tcDgU1fzGaQXmR8h++nXvILJ+r5SmOXaaL/2pocunQo2a8TAXhjQnBpRLPtZ1KCz/TYpqeNuE2ew=="
},
"inter-ui": { "inter-ui": {
"version": "3.15.0", "version": "3.18.1",
"resolved": "https://registry.npmjs.org/inter-ui/-/inter-ui-3.15.0.tgz", "resolved": "https://registry.npmjs.org/inter-ui/-/inter-ui-3.18.1.tgz",
"integrity": "sha512-6v0WK8FHkVYbNQZ7L9O5tP8280pgTBR9ydxqYwssMuUH6SZO70ZFK/NQ1Ob8nNmOOzpUJAzT0WE73ty96z1tAQ==" "integrity": "sha512-W3LnAirp6a1ixpAHZwr9gH52KlOQOAp0oqbmIoGi2dAIlcIB7auJgLr9XFHUzYy2FoZ0Nf7aPe/nHMZB4/Zvdg=="
}, },
"litepicker": { "litepicker": {
"version": "1.5.7", "version": "2.0.11",
"resolved": "https://registry.npmjs.org/litepicker/-/litepicker-1.5.7.tgz", "resolved": "https://registry.npmjs.org/litepicker/-/litepicker-2.0.11.tgz",
"integrity": "sha512-4L2ZcF8iqCE4A/qGWS3PbdFplZR1g751x5SsZ87zCRZ4LQN1Fgezarnvqi0eHk/kDWK7Qx0HZ9Y4bNznJMF1xA==" "integrity": "sha512-7MECMp2EDGIYDIz9QT24t9hWpgBD9JD57ZdDrbffNMGfbw0JVhBhvlYsyaIUuYhywtLvgmI5lfulM7XF2HLEkg=="
}, },
"svg.draggable.js": { "svg.draggable.js": {
"version": "2.2.2", "version": "2.2.2",
@@ -99,11 +245,6 @@
"requires": { "requires": {
"svg.js": "^2.6.5" "svg.js": "^2.6.5"
} }
},
"turbolinks": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/turbolinks/-/turbolinks-5.2.0.tgz",
"integrity": "sha512-pMiez3tyBo6uRHFNNZoYMmrES/IaGgMhQQM+VFF36keryjb5ms0XkVpmKHkfW/4Vy96qiGW3K9bz0tF5sK9bBw=="
} }
} }
} }

View File

@@ -18,10 +18,10 @@
"homepage": "https://github.com/milesmcc/shynet#readme", "homepage": "https://github.com/milesmcc/shynet#readme",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1", "@fortawesome/fontawesome-free": "^5.15.1",
"a17t": "^0.2.9", "a17t": "^0.5.1",
"apexcharts": "^3.22.0", "apexcharts": "^3.24.0",
"flag-icon-css": "^3.5.0",
"inter-ui": "^3.15.0", "inter-ui": "^3.15.0",
"litepicker": "^1.5.7", "litepicker": "^2.0.11"
"turbolinks": "^5.2.0"
} }
} }

View File

@@ -1,12 +1,12 @@
<nav class="flex w-full flex-wrap items-center justify-between" role="navigation" aria-label="pagination"> <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"> <div class="w-full md:w-auto mb-2">
{% if page.has_previous %} {% 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 %} {% else %}
<a class="button field bg-neutral-000 w-auto mr-1" disabled>Previous</a> <a class="button field bg-neutral-000 w-auto mr-1" disabled>Previous</a>
{% endif %} {% endif %}
{% if page.has_next %} {% 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 %} {% else %}
<a class="button field bg-neutral-000 w-auto" disabled>Next</a> <a class="button field bg-neutral-000 w-auto" disabled>Next</a>
{% endif %} {% endif %}
@@ -17,7 +17,7 @@
{% ifequal page.number pnum %} {% ifequal page.number pnum %}
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li> <li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %} {% 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 %} {% endifequal %}
{% endfor %} {% endfor %}
@@ -27,7 +27,7 @@
{% ifequal page.number pnum %} {% ifequal page.number pnum %}
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li> <li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %} {% 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 %} {% endifequal %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@@ -38,7 +38,7 @@
{% ifequal page.number pnum %} {% ifequal page.number pnum %}
<li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li> <li><a class="button field w-auto mx-1 text-white bg-neutral-700">{{ pnum }}</a></li>
{% else %} {% 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 %} {% endifequal %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}

View File

@@ -15,12 +15,8 @@ def pagination(
before_current_pages=4, before_current_pages=4,
after_current_pages=4, after_current_pages=4,
): ):
url_parameters = "".join( url_parameters = urlencode(
[ [(key, value) for key, value in request.GET.items() if key != "page"]
f"&{urlencode(key)}={urlencode(value)}"
for key, value in request.GET.items()
if key != "page"
]
) )
before = max(page.number - before_current_pages - 1, 0) before = max(page.number - before_current_pages - 1, 0)

View File

@@ -60,7 +60,9 @@ class Migration(migrations.Migration):
), ),
), ),
], ],
options={"ordering": ["-start_time"],}, options={
"ordering": ["-start_time"],
},
), ),
migrations.CreateModel( migrations.CreateModel(
name="Hit", name="Hit",
@@ -90,7 +92,9 @@ class Migration(migrations.Migration):
), ),
), ),
], ],
options={"ordering": ["-start_time"],}, options={
"ordering": ["-start_time"],
},
), ),
migrations.AddIndex( migrations.AddIndex(
model_name="session", model_name="session",

View 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"
),
),
]

View 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),
),
]

View 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"
),
),
]

View 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"
),
),
]

View 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),
)
]

View 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: ()),
]

View File

@@ -1,11 +1,10 @@
import json
import uuid import uuid
from django.db import models from django.db import models
from django.shortcuts import reverse from django.shortcuts import reverse
from django.utils import timezone from django.utils import timezone
from core.models import Service from core.models import Service, ACTIVE_USER_TIMEDELTA
def _default_uuid(): def _default_uuid():
@@ -20,8 +19,8 @@ class Session(models.Model):
identifier = models.TextField(blank=True, db_index=True) identifier = models.TextField(blank=True, db_index=True)
# Time # Time
start_time = models.DateTimeField(auto_now_add=True, db_index=True) start_time = models.DateTimeField(default=timezone.now, db_index=True)
last_seen = models.DateTimeField(auto_now_add=True) last_seen = models.DateTimeField(default=timezone.now, db_index=True)
# Core request information # Core request information
user_agent = models.TextField() user_agent = models.TextField()
@@ -48,16 +47,19 @@ class Session(models.Model):
latitude = models.FloatField(null=True) latitude = models.FloatField(null=True)
time_zone = models.TextField(blank=True) time_zone = models.TextField(blank=True)
is_bounce = models.BooleanField(default=True, db_index=True)
class Meta: class Meta:
ordering = ["-start_time"] ordering = ["-start_time"]
indexes = [ indexes = [
models.Index(fields=["service", "-start_time"]), models.Index(fields=["service", "-start_time"]),
models.Index(fields=["service", "-last_seen"]),
models.Index(fields=["service", "identifier"]), models.Index(fields=["service", "identifier"]),
] ]
@property @property
def is_currently_active(self): 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 @property
def duration(self): def duration(self):
@@ -72,14 +74,20 @@ class Session(models.Model):
kwargs={"pk": self.service.pk, "session_pk": self.uuid}, 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): class Hit(models.Model):
session = models.ForeignKey(Session, on_delete=models.CASCADE, db_index=True) session = models.ForeignKey(Session, on_delete=models.CASCADE, db_index=True)
initial = models.BooleanField(default=True, db_index=True) initial = models.BooleanField(default=True, db_index=True)
# Base request information # Base request information
start_time = models.DateTimeField(auto_now_add=True, db_index=True) start_time = models.DateTimeField(default=timezone.now, db_index=True)
last_seen = models.DateTimeField(auto_now_add=True) last_seen = models.DateTimeField(default=timezone.now, db_index=True)
heartbeats = models.IntegerField(default=0) heartbeats = models.IntegerField(default=0)
tracker = models.TextField( tracker = models.TextField(
choices=[("JS", "JavaScript"), ("PIXEL", "Pixel (noscript)")] choices=[("JS", "JavaScript"), ("PIXEL", "Pixel (noscript)")]
@@ -88,12 +96,17 @@ class Hit(models.Model):
# Advanced page information # Advanced page information
location = models.TextField(blank=True, db_index=True) location = models.TextField(blank=True, db_index=True)
referrer = 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: class Meta:
ordering = ["-start_time"] ordering = ["-start_time"]
indexes = [ indexes = [
models.Index(fields=["session", "-start_time"]), models.Index(fields=["session", "-start_time"]),
models.Index(fields=["service", "-start_time"]),
models.Index(fields=["session", "location"]), models.Index(fields=["session", "location"]),
models.Index(fields=["session", "referrer"]), models.Index(fields=["session", "referrer"]),
] ]
@@ -105,5 +118,5 @@ class Hit(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse( return reverse(
"dashboard:service_session", "dashboard:service_session",
kwargs={"pk": self.session.service.pk, "session_pk": self.session.pk}, kwargs={"pk": self.service.pk, "session_pk": self.session.pk},
) )

View File

@@ -1,5 +1,4 @@
import ipaddress import ipaddress
import json
import logging import logging
from hashlib import sha256 from hashlib import sha256
@@ -40,6 +39,9 @@ def _geoip2_lookup(ip):
} }
except geoip2.errors.AddressNotFoundError: except geoip2.errors.AddressNotFoundError:
return {} return {}
except FileNotFoundError as e:
log.exception("Unable to perform GeoIP lookup: %s", e)
return {}
@shared_task @shared_task
@@ -59,6 +61,7 @@ def ingress_request(
log.debug(f"Linked to service {service}") log.debug(f"Linked to service {service}")
if dnt and service.respect_dnt: if dnt and service.respect_dnt:
log.debug("Ignoring because of DNT")
return return
try: try:
@@ -68,6 +71,7 @@ def ingress_request(
ignored_network.version == remote_ip.version ignored_network.version == remote_ip.version
and ignored_network.supernet_of(remote_ip) and ignored_network.supernet_of(remote_ip)
): ):
log.debug("Ignoring because of ignored IP")
return return
except ValueError as e: except ValueError as e:
log.exception(e) log.exception(e)
@@ -79,6 +83,11 @@ def ingress_request(
association_id_hash = sha256() association_id_hash = sha256()
association_id_hash.update(str(ip).encode("utf-8")) association_id_hash.update(str(ip).encode("utf-8"))
association_id_hash.update(str(user_agent).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 = ( session_cache_path = (
f"session_association_{service.pk}_{association_id_hash.hexdigest()}" f"session_association_{service.pk}_{association_id_hash.hexdigest()}"
) )
@@ -117,12 +126,14 @@ def ingress_request(
return return
session = Session.objects.create( session = Session.objects.create(
service=service, 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, user_agent=user_agent,
identifier=identifier.strip(), identifier=identifier.strip(),
browser=ua.browser.family or "", browser=ua.browser.family or "",
device=ua.device.family or ua.device.model or "", device=ua.device.family or ua.device.model or "",
device_type=device_type, device_type=device_type,
start_time=time,
last_seen=time,
os=ua.os.family or "", os=ua.os.family or "",
asn=ip_data.get("asn") or "", asn=ip_data.get("asn") or "",
country=ip_data.get("country") or "", country=ip_data.get("country") or "",
@@ -139,7 +150,7 @@ def ingress_request(
log.debug("Updating old session with new data...") log.debug("Updating old session with new data...")
# Update last seen time # Update last seen time
session.last_seen = timezone.now() session.last_seen = time
if session.identifier == "" and identifier.strip() != "": if session.identifier == "" and identifier.strip() != "":
session.identifier = identifier.strip() session.identifier = identifier.strip()
session.save() session.save()
@@ -160,7 +171,7 @@ def ingress_request(
# this is a heartbeat. # this is a heartbeat.
log.debug("Hit is a heartbeat; updating old hit with new data...") log.debug("Hit is a heartbeat; updating old hit with new data...")
hit.heartbeats += 1 hit.heartbeats += 1
hit.last_seen = timezone.now() hit.last_seen = time
hit.save() hit.save()
if hit is None: if hit is None:
@@ -176,7 +187,14 @@ def ingress_request(
location=payload.get("location", location), location=payload.get("location", location),
referrer=payload.get("referrer", ""), referrer=payload.get("referrer", ""),
load_time=payload.get("loadTime"), 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) # Set idempotency (if applicable)
if idempotency is not None: if idempotency is not None:
cache.set( cache.set(
@@ -184,4 +202,5 @@ def ingress_request(
) )
except Exception as e: except Exception as e:
log.exception(e) log.exception(e)
print(e)
raise e raise e

View File

@@ -8,11 +8,14 @@
var Shynet = { var Shynet = {
idempotency: null, idempotency: null,
heartbeatTaskId: null, heartbeatTaskId: null,
skipHeartbeat: false,
sendHeartbeat: function () { sendHeartbeat: function () {
try { try {
if (document.hidden) { if (document.hidden || Shynet.skipHeartbeat) {
return; return;
} }
Shynet.skipHeartbeat = true;
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.open( xhr.open(
"POST", "POST",
@@ -20,6 +23,12 @@ var Shynet = {
true true
); );
xhr.setRequestHeader("Content-Type", "application/json"); xhr.setRequestHeader("Content-Type", "application/json");
xhr.onload = function () {
Shynet.skipHeartbeat = false;
};
xhr.onerror = function () {
Shynet.skipHeartbeat = false;
};
xhr.send( xhr.send(
JSON.stringify({ JSON.stringify({
idempotency: Shynet.idempotency, idempotency: Shynet.idempotency,
@@ -30,13 +39,14 @@ var Shynet = {
window.performance.timing.navigationStart, window.performance.timing.navigationStart,
}) })
); );
} catch (e) { } } catch (e) {}
}, },
newPageLoad: function () { newPageLoad: function () {
if (Shynet.heartbeatTaskId != null) { if (Shynet.heartbeatTaskId != null) {
clearInterval(Shynet.heartbeatTaskId); clearInterval(Shynet.heartbeatTaskId);
} }
Shynet.idempotency = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); Shynet.idempotency = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
Shynet.skipHeartbeat = false;
Shynet.heartbeatTaskId = setInterval(Shynet.sendHeartbeat, parseInt("{{heartbeat_frequency}}")); Shynet.heartbeatTaskId = setInterval(Shynet.sendHeartbeat, parseInt("{{heartbeat_frequency}}"));
Shynet.sendHeartbeat(); Shynet.sendHeartbeat();
} }
@@ -51,4 +61,4 @@ window.addEventListener("load", Shynet.newPageLoad);
// -- START -- // -- START --
{{script_inject|safe}} {{script_inject|safe}}
// -- END -- // -- END --
{% endif %} {% endif %}

View File

@@ -5,7 +5,12 @@ from urllib.parse import urlparse
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError 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.shortcuts import render, reverse
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
@@ -49,21 +54,24 @@ class ValidateServiceOriginsMixin:
origins = service.origins origins = service.origins
cache.set(f"service_origins_{service_uuid}", origins, timeout=3600) cache.set(f"service_origins_{service_uuid}", origins, timeout=3600)
resp = super().dispatch(request, *args, **kwargs) allow_origin = "*"
if origins != "*": if origins != "*":
remote_origin = request.META.get("HTTP_ORIGIN") 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")) parsed = urlparse(request.META.get("HTTP_REFERER"))
remote_origin = f"{parsed.scheme}://{parsed.netloc}".lower() remote_origin = f"{parsed.scheme}://{parsed.netloc}".lower()
origins = [origin.strip().lower() for origin in origins.split(",")] origins = [origin.strip().lower() for origin in origins.split(",")]
if remote_origin in origins: if remote_origin in origins:
resp["Access-Control-Allow-Origin"] = remote_origin allow_origin = remote_origin
else: else:
return HttpResponseForbidden() 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-Methods"] = "GET,HEAD,OPTIONS,POST"
resp[ resp[
"Access-Control-Allow-Headers" "Access-Control-Allow-Headers"
@@ -104,7 +112,9 @@ class ScriptView(ValidateServiceOriginsMixin, View):
endpoint = ( endpoint = (
reverse( reverse(
"ingress:endpoint_script", "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 if self.kwargs.get("identifier") == None
else reverse( else reverse(

View 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!"))

View File

@@ -14,7 +14,8 @@ class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
"hostname", type=str, "hostname",
type=str,
) )
def handle(self, *args, **options): def handle(self, *args, **options):

View File

@@ -14,7 +14,8 @@ class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
"email", type=str, "email",
type=str,
) )
def handle(self, *args, **options): def handle(self, *args, **options):

View File

@@ -14,7 +14,8 @@ class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
"name", type=str, "name",
type=str,
) )
def handle(self, *args, **options): def handle(self, *args, **options):

View File

@@ -112,7 +112,9 @@ class Migration(migrations.Migration):
"verbose_name_plural": "users", "verbose_name_plural": "users",
"abstract": False, "abstract": False,
}, },
managers=[("objects", django.contrib.auth.models.UserManager()),], managers=[
("objects", django.contrib.auth.models.UserManager()),
],
), ),
migrations.CreateModel( migrations.CreateModel(
name="Service", name="Service",

View File

@@ -11,6 +11,7 @@ class Migration(migrations.Migration):
operations = [ operations = [
migrations.AlterModelOptions( migrations.AlterModelOptions(
name="service", options={"ordering": ["name", "uuid"]}, name="service",
options={"ordering": ["name", "uuid"]},
), ),
] ]

View File

@@ -6,18 +6,20 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('core', '0007_service_ignore_robots'), ("core", "0007_service_ignore_robots"),
] ]
operations = [ operations = [
migrations.AddField( migrations.AddField(
model_name='service', model_name="service",
name='script_inject', name="script_inject",
field=models.TextField(blank=True, default=''), field=models.TextField(blank=True, default=""),
), ),
migrations.AlterField( migrations.AlterField(
model_name='user', model_name="user",
name='first_name', name="first_name",
field=models.CharField(blank=True, max_length=150, verbose_name='first name'), field=models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
), ),
] ]

View File

@@ -4,14 +4,20 @@ import re
import uuid import uuid
from django.apps import apps from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models.functions import TruncDate from django.db.models.functions import TruncDate, TruncHour
from django.db.utils import NotSupportedError from django.db.utils import NotSupportedError
from django.shortcuts import reverse from django.shortcuts import reverse
from django.utils import timezone from django.utils import timezone
# How long a session a needs to go without an update to no longer be considered 'active' (i.e., currently online)
ACTIVE_USER_TIMEDELTA = timezone.timedelta(
milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2
)
def _default_uuid(): def _default_uuid():
return str(uuid.uuid4()) return str(uuid.uuid4())
@@ -119,8 +125,10 @@ class Service(models.Model):
Session = apps.get_model("analytics", "Session") Session = apps.get_model("analytics", "Session")
Hit = apps.get_model("analytics", "Hit") Hit = apps.get_model("analytics", "Hit")
tz_now = timezone.now()
currently_online = Session.objects.filter( currently_online = Session.objects.filter(
service=self, last_seen__gt=timezone.now() - timezone.timedelta(seconds=10) service=self, last_seen__gt=tz_now - ACTIVE_USER_TIMEDELTA
).count() ).count()
sessions = Session.objects.filter( sessions = Session.objects.filter(
@@ -129,11 +137,13 @@ class Service(models.Model):
session_count = sessions.count() session_count = sessions.count()
hits = Hit.objects.filter( 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() 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() bounce_count = bounces.count()
locations = ( locations = (
@@ -202,22 +212,41 @@ class Service(models.Model):
if session_count == 0: if session_count == 0:
avg_session_duration = None avg_session_duration = None
session_chart_data = { # Show hourly chart for date ranges of 3 days or less, otherwise daily chart
k["date"]: k["count"] if (end_time - start_time).days < 3:
for k in sessions.annotate(date=TruncDate("start_time")) session_chart_tooltip_format = "MM/dd HH:mm"
.values("date") session_chart_granularity = "hourly"
.annotate(count=models.Count("uuid")) session_chart_data = {
.order_by("date") k["hour"]: k["count"]
} for k in sessions.annotate(hour=TruncHour("start_time"))
for day_offset in range((end_time - start_time).days + 1): .values("hour")
day = (start_time + timezone.timedelta(days=day_offset)).date() .annotate(count=models.Count("uuid"))
if day not in session_chart_data: .order_by("hour")
session_chart_data[day] = 0 }
for hour_offset in range(int((end_time - start_time).total_seconds() / 3600) + 1):
hour = (start_time + timezone.timedelta(hours=hour_offset))
if hour not in session_chart_data:
session_chart_data[hour] = 0 if hour <= tz_now else None
else:
session_chart_tooltip_format = "MMM d"
session_chart_granularity = "daily"
session_chart_data = {
k["date"]: k["count"]
for k in sessions.annotate(date=TruncDate("start_time"))
.values("date")
.annotate(count=models.Count("uuid"))
.order_by("date")
}
for day_offset in range((end_time - start_time).days + 1):
day = (start_time + timezone.timedelta(days=day_offset)).date()
if day not in session_chart_data:
session_chart_data[day] = 0 if day <= tz_now.date() else None
return { return {
"currently_online": currently_online, "currently_online": currently_online,
"session_count": session_count, "session_count": session_count,
"hit_count": hit_count, "hit_count": hit_count,
"has_hits": has_hits,
"avg_hits_per_session": hit_count / (max(session_count, 1)), "avg_hits_per_session": hit_count / (max(session_count, 1)),
"bounce_rate_pct": bounce_count * 100 / session_count "bounce_rate_pct": bounce_count * 100 / session_count
if session_count > 0 if session_count > 0
@@ -240,8 +269,13 @@ class Service(models.Model):
) )
] ]
), ),
"session_chart_tooltip_format": session_chart_tooltip_format,
"session_chart_granularity": session_chart_granularity,
"online": True, "online": True,
} }
def get_absolute_url(self): def get_absolute_url(self):
return reverse("dashboard:service", kwargs={"pk": self.pk},) return reverse(
"dashboard:service",
kwargs={"pk": self.pk},
)

View File

@@ -1,5 +1,6 @@
from allauth.account.admin import EmailAddress from allauth.account.admin import EmailAddress
from django import forms from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import Service, User from core.models import Service, User
@@ -18,7 +19,7 @@ class ServiceForm(forms.ModelForm):
"hide_referrer_regex", "hide_referrer_regex",
"origins", "origins",
"collaborators", "collaborators",
"script_inject" "script_inject",
] ]
widgets = { widgets = {
"name": forms.TextInput(), "name": forms.TextInput(),
@@ -28,12 +29,11 @@ class ServiceForm(forms.ModelForm):
"collect_ips": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]), "collect_ips": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
"ignore_robots": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]), "ignore_robots": forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
"hide_referrer_regex": forms.TextInput(), "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 = { labels = {
"origins": "Allowed origins", "origins": "Allowed origins",
"respect_dnt": "Respect DNT", "respect_dnt": "Respect DNT",
"collect_ips": "Collect IP addresses",
"ignored_ips": "Ignored IP addresses", "ignored_ips": "Ignored IP addresses",
"ignore_robots": "Ignore robots", "ignore_robots": "Ignore robots",
"hide_referrer_regex": "Hide specific referrers", "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)." "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?", "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').", "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?", "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.", "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.", "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( collaborators = forms.CharField(
help_text="Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)", help_text="Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)",
required=False, required=False,
@@ -60,7 +74,9 @@ class ServiceForm(forms.ModelForm):
def clean_collaborators(self): def clean_collaborators(self):
collaborators = [] 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(","): for collaborator_email in self.cleaned_data["collaborators"].split(","):
email = collaborator_email.strip() email = collaborator_email.strip()
if email == "": if email == "":
@@ -72,7 +88,9 @@ class ServiceForm(forms.ModelForm):
raise forms.ValidationError(f"Email '{email}' is not registered") raise forms.ValidationError(f"Email '{email}' is not registered")
user = collaborator_email_linked.user user = collaborator_email_linked.user
if user in collaborators: 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 users_to_emails[user] = email
collaborators.append(collaborator_email_linked.user) collaborators.append(collaborator_email_linked.user)
return collaborators return collaborators

View File

@@ -1,12 +1,11 @@
from datetime import datetime, time from datetime import datetime, time
from urllib.parse import urlparse
from django.utils import timezone from django.utils import timezone
class DateRangeMixin: class DateRangeMixin:
def get_start_date(self): def get_start_date(self):
if self.request.GET.get("startDate") != None: if self.request.GET.get("startDate") is not None:
found_time = timezone.datetime.strptime( found_time = timezone.datetime.strptime(
self.request.GET.get("startDate"), "%Y-%m-%d" self.request.GET.get("startDate"), "%Y-%m-%d"
) )
@@ -15,7 +14,7 @@ class DateRangeMixin:
return timezone.now() - timezone.timedelta(days=30) return timezone.now() - timezone.timedelta(days=30)
def get_end_date(self): def get_end_date(self):
if self.request.GET.get("endDate") != None: if self.request.GET.get("endDate") is not None:
found_time = timezone.datetime.strptime( found_time = timezone.datetime.strptime(
self.request.GET.get("endDate"), "%Y-%m-%d" self.request.GET.get("endDate"), "%Y-%m-%d"
) )
@@ -23,8 +22,45 @@ class DateRangeMixin:
else: else:
return timezone.now() return timezone.now()
def get_date_ranges(self):
now = timezone.now()
return [
{
'name': 'Last 3 days',
'start': now - timezone.timedelta(days=2),
'end': now,
},
{
'name': 'Last 30 days',
'start': now - timezone.timedelta(days=29),
'end': now,
},
{
'name': 'Last 90 days',
'start': now - timezone.timedelta(days=89),
'end': now,
},
{
'name': 'This month',
'start': now.replace(day=1),
'end': now,
},
{
'name': 'Last month',
'start': now.replace(day=1, month=now.month - 1),
'end': now.replace(day=1, month=now.month) - timezone.timedelta(days=1),
},
{
'name': 'This year',
'start': now.replace(day=1, month=1),
'end': now,
},
]
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
data["start_date"] = self.get_start_date() data["start_date"] = self.get_start_date()
data["end_date"] = self.get_end_date() data["end_date"] = self.get_end_date()
data["date_ranges"] = self.get_date_ranges()
return data return data

View File

@@ -13,4 +13,26 @@
.rf { .rf {
text-align: right !important; text-align: right !important;
}
.chart-card .apexcharts-svg {
border-radius: 0 0 var(--border-radius-lg, 0.5rem) var(--border-radius-lg, 0.5rem);
}
.max-w-0 {
max-width: 0;
}
:root {
--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;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -1,8 +1,8 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<div> <div class="flex-1 truncate">
<h4 class="heading">{% block page_title %}{% endblock %}</h4> <h4 class="heading truncate">{% block page_title %}{% endblock %}</h4>
</div> </div>
<hr class="sep"> <hr class="sep">
{% block main %} {% block main %}

View File

@@ -8,31 +8,36 @@
<meta name="robots" content="noindex"> <meta name="robots" content="noindex">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
{% include 'a17t/includes/head.html' %} {% 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 'apexcharts/dist/apexcharts.min.js'%}"></script>
<script src="{% static 'litepicker/dist/js/main.js' %}"></script> <script src="{% static 'litepicker/dist/nocss/litepicker.js' %}"></script>
<script src="{% static 'turbolinks/dist/turbolinks.js' %}"></script> <script src="{% static 'litepicker/dist/plugins/ranges.js' %}"></script>
<link rel="stylesheet" href="{% static 'litepicker/dist/css/litepicker.css' %}">
<script src="{% static 'dashboard/js/base.js' %}"></script> <script src="{% static 'dashboard/js/base.js' %}"></script>
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}"> <link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
<link rel="stylesheet" href="{% static 'flag-icon-css/css/flag-icon.min.css' %}">
{% block extra_head %} {% block extra_head %}
{% endblock %} {% endblock %}
</head> </head>
<body class="bg-neutral-200 min-h-full"> <body class="bg-neutral-100 min-h-full overflow-x-hidden">
{% block body %} {% block body %}
<section class="max-w-screen-xl mx-auto px-4 py-4 md:py-12 md:flex"> <section class="max-w-screen-xl mx-auto px-4 py-4 md:py-12 md:flex">
<aside 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' %}"> <a class="icon ~urge ml-2 md:ml-6 md:mb-8 md:mt-3" href="{% url 'dashboard:dashboard' %}">
<i class="fas fa-binoculars fa-3x text-urge-600 hidden md:block"></i> <i class="fas fa-binoculars fa-3x text-urge-600 hidden md:block"></i>
<i class="fas fa-binoculars fa-2x text-urge-600 md:hidden"></i> <i class="fas fa-binoculars fa-2x text-urge-600 md:hidden"></i>
</a> </a>
<button class="button ~neutral !low md:hidden" <a tabindex="0" role="button" class="button ~neutral !low md:hidden"
onclick="document.getElementById('navMenuExpanded').classList.toggle('hidden')"> onclick="document.getElementById('navMenuExpanded').classList.toggle('hidden')">
<span class="icon"> <span class="icon">
<i class="fas fa-bars"></i> <i class="fas fa-bars"></i>
</span> </span>
</button> </a>
<hr class="sep h-4 md:h-8 w-full"> <hr class="sep h-4 md:h-8 w-full">
<div id="navMenuExpanded" <div id="navMenuExpanded"
class="bg-neutral-000 shadow-lg md:shadow-none p-4 hidden rounded-lg md:block md:bg-transparent md:border-none md:p-0 w-full"> class="bg-neutral-000 shadow-lg md:shadow-none p-4 hidden rounded-lg md:block md:bg-transparent md:border-none md:p-0 w-full">
@@ -40,8 +45,8 @@
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Services</p> <p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Services</p>
{% for service in user.owning_services.all %} {% for service in user.owning_services.all %}
{% url 'dashboard:service' service.uuid as url %} {% contextual_url 'dashboard:service' service.uuid as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name|truncatechars:20 url=url %} {% include 'dashboard/includes/sidebar_portal.html' with label=service.name url=url icon=service.link|iconify %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
@@ -59,8 +64,8 @@
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Collaborations</p> <p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Collaborations</p>
{% for service in user.collaborating_services.all %} {% for service in user.collaborating_services.all %}
{% url 'dashboard:service' service.uuid as url %} {% contextual_url 'dashboard:service' service.uuid as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name|truncatechars:20 url=url %} {% include 'dashboard/includes/sidebar_portal.html' with label=service.name url=url %}
{% endfor %} {% endfor %}
<hr class="sep h-8"> <hr class="sep h-8">
@@ -120,4 +125,4 @@
{% endblock %} {% endblock %}
</body> </body>
</html> </html>

View File

@@ -2,32 +2,48 @@
<input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate"> <input type="hidden" name="startDate" value="{{start_date.isoformat}}" id="startDate">
<input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate"> <input type="hidden" name="endDate" value="{{end_date.isoformat}}" id="endDate">
</form> </form>
<input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral bg-neutral-000 cursor-pointer" readonly> <input type="input" id="rangePicker" placeholder="Date range" class="input ~neutral bg-neutral-000 cursor-pointer w-auto" readonly>
<style> <style>
:root { :root {
--litepickerMonthButtonHover: var(--color-urge); --litepicker-button-prev-month-color-hover: var(--color-urge);
--litepickerDayColorHover: var(--color-urge); --litepicker-button-next-month-color-hover: var(--color-urge);
--litepickerDayIsTodayColor: var(--color-urge); --litepicker-day-color-hover: var(--color-urge);
--litepickerDayIsInRange: var(--color-urge-normal-fill); --litepicker-is-today-color: var(--color-urge);
--litepickerDayIsStartBg: var(--color-urge); --litepicker-is-in-range-color: var(--color-urge-normal-fill);
--litepickerDayIsEndBg: var(--color-urge); --litepicker-is-start-color-bg: var(--color-urge);
--litepickerButtonApplyBg: var(--color-urge); --litepicker-is-end-color-bg: var(--color-urge);
--litepicker-button-apply-color-bg: var(--color-urge);
}
.litepicker .container__predefined-ranges, .litepicker .container__months {
box-shadow: var(--fallback-box-shadow-normal) !important;
} }
</style> </style>
<script> <script>
var picker = new Litepicker({ var picker = new Litepicker({
element: document.getElementById('rangePicker'), element: document.getElementById('rangePicker'),
plugins: ['ranges'],
singleMode: false, singleMode: false,
format: 'MMM D, YYYY', format: "MMM DD 'YY",
maxDate: new Date(), maxDate: new Date(),
startDate: Date.parse(document.getElementById("startDate").getAttribute("value")), startDate: Date.parse(document.getElementById("startDate").getAttribute("value")),
endDate: Date.parse(document.getElementById("endDate").getAttribute("value")), endDate: Date.parse(document.getElementById("endDate").getAttribute("value")),
onSelect: function (startDate, endDate) { ranges: {
document.getElementById("startDate").setAttribute("value", startDate.getFullYear() + customRanges: {
"-" + (startDate.getMonth() + 1) + "-" + startDate.getDate()); {% for date_range in date_ranges %}
document.getElementById("endDate").setAttribute("value", endDate.getFullYear() + "-" + '{{ date_range.name }}': [
(endDate.getMonth() + 1) + "-" + endDate.getDate()); new Date('{{ date_range.start.isoformat }}'),
document.getElementById("datePicker").submit(); 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>

View File

@@ -1,15 +1,16 @@
{% load humanize helpers %} {% 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 %} {% with stats=object.stats %}
<div class="p-4 md:flex justify-between"> <div class="p-4 md:flex justify-between overflow-none">
<div class="flex items-center mb-4 md:mb-0"> <div class="flex items-center mb-4 md:mb-0 md:flex-1 md:min-w-0 truncate pr-0 md:pr-2">
<h3 class="heading text-xl md:text-2xl mr-2 mb-1 text-urge-600"> <h3 class="heading text-xl md:text-2xl mr-2 mb-1 text-urge-600 flex items-center truncate" title="{{object.name}}">
{{object.name}} {{object.link|iconify}}
<span class="truncate">{{object.name}}</span>
</h3> </h3>
{% include 'dashboard/includes/stats_status_chip.html' %} {% include 'dashboard/includes/stats_status_chip.html' %}
</div> </div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6"> <div class="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-3 lg:gap-6 md:flex-none">
<div> <div>
<p>Sessions</p> <p>Sessions</p>
<p class="label"> <p class="label">
@@ -50,7 +51,7 @@
</div> </div>
<hr class="sep h-4"> <hr class="sep h-4">
<div style="bottom: -1px;"> <div style="bottom: -1px;">
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data sparkline=True height=100 name=object.uuid %} {% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data sparkline=True height=100 name=object.uuid tooltip_format=stats.session_chart_tooltip_format granularity=stats.session_chart_granularity %}
</div> </div>
{% endwith %} {% endwith %}
</a> </a>

View File

@@ -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>

View File

@@ -12,7 +12,7 @@
{% for session in object_list %} {% for session in object_list %}
<tr> <tr>
<td> <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"> class="font-medium text-urge-700">
{{session.start_time|date:"M j Y, g:i a"|capfirst}} {{session.start_time|date:"M j Y, g:i a"|capfirst}}
{% if session.is_currently_active %} {% if session.is_currently_active %}
@@ -27,7 +27,7 @@
<span class="text-gray-600">&mdash;</span> <span class="text-gray-600">&mdash;</span>
{% endif %} {% endif %}
</td> </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.duration|naturaldelta}}</td>
<td class="rf">{{session.hit_set.count|intcomma}}</td> <td class="rf">{{session.hit_set.count|intcomma}}</td>
</tr> </tr>

View File

@@ -1,6 +1,6 @@
{% load helpers %} {% load helpers %}
<div> <div>
<a class="portal !low {% if request.get_full_path|startswith:url %}~urge active bg-neutral-100{% endif %}" <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}}">{{label}}</a> {% if disable_turbolinks %}data-turbolinks="false"{% endif %} href="{{url}}">{{icon}} <span class="truncate">{{label}}</span></a>
</div> </div>

View File

@@ -2,7 +2,7 @@
{% with stats=object.get_daily_stats %} {% with stats=object.get_daily_stats %}
{% if stats.currently_online > 0 %} {% if stats.currently_online > 0 %}
<span class="chip ~positive !high"> <span class="chip ~positive !high whitespace-nowrap">
{{stats.currently_online|intcomma}} online {{stats.currently_online|intcomma}} online
</span> </span>
{% endif %} {% endif %}

View File

@@ -6,6 +6,9 @@
}, },
tooltip: { tooltip: {
shared: false, shared: false,
x: {
format: '{{tooltip_format|default:"MMM d"}}',
},
}, },
colors: ["#805AD5"], colors: ["#805AD5"],
chart: { chart: {
@@ -34,6 +37,14 @@
stops: [0, 75, 100] stops: [0, 75, 100]
}, },
}, },
{% if granularity == "daily" and click_zoom %}
events: {
markerClick: function(event, chartContext, { seriesIndex, dataPointIndex, w: {config}}) {
const day = config.series[seriesIndex].data[dataPointIndex].x
window.location.href = `?startDate=${day}&endDate=${day}`
},
},
{% endif %}
}, },
grid: { grid: {
padding: { padding: {
@@ -63,6 +74,9 @@
}, },
xaxis: { xaxis: {
type: "datetime", type: "datetime",
labels: {
datetimeUTC: false
},
}, },
stroke: { stroke: {
width: 1.5, width: 1.5,

View File

@@ -1,11 +1,11 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load rules %} {% load rules pagination %}
{% block content %} {% block content %}
<div class="md:flex justify-between items-center"> <div class="md:flex justify-between items-center">
<div> <div class="flex-1 truncate display-none md:display-block mr-4 md:mb-0 mb-4" title="{{request.site.name|default:"Dashboard"}}">
<h4 class="heading">{{request.site.name|default:"Dashboard"}}</h4> <h4 class="heading truncate">{{request.site.name|default:"Dashboard"}}</h4>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<div class="mr-1"> <div class="mr-1">
@@ -17,10 +17,15 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
<hr class="sep"> <hr class="sep h-8 md:h-12">
{% for object in services|dictsortreversed:"stats.session_count" %} {% for object in object_list|dictsortreversed:"stats.session_count" %}
{% include 'dashboard/includes/service_overview.html' %} {% include 'dashboard/includes/service_overview.html' %}
{% empty %} {% empty %}
<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 %} {% endfor %}
{% if object_list %}
{% pagination page_obj request %}
{% endif %}
{% endblock %} {% endblock %}

View File

@@ -6,29 +6,37 @@
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div> <div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
{% has_perm 'core.change_service' user object as can_update %} {% has_perm 'core.change_service' user object as can_update %}
{% if can_update %} {% if can_update %}
<a href="{% url 'dashboard:service_update' service.uuid %}" class="button field bg-neutral-000 w-auto">Manage &rarr;</a> <a href="{% contextual_url 'dashboard:service_update' service.uuid %}" class="button field bg-neutral-000 w-auto">Manage &rarr;</a>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block service_content %} {% 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>&lt;body&gt;</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"> <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" %} {% with classes="text-sm font-semibold" good_classes="text-positive-400" bad_classes="text-critical-400" neutral_classes="text-gray-400" %}
<article class=""> <article class="">
<p class="label text-gray-400">Sessions</p> <p class="label text-gray-400">Sessions</p>
<p class="heading"> <p class="heading">
{{stats.session_count|intcomma}} {{stats.session_count|intcomma}}
<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 %} {% 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>
</p> </p>
</article> </article>
<article class=""> <article class="">
<p class="label text-gray-400">Hits</p> <p class="label text-gray-400">Hits</p>
<p class="heading"> <p class="heading">
{{stats.hit_count|intcomma}} {{stats.hit_count|intcomma}}
<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 %} {% 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>
</p> </p>
</article> </article>
<article class=""> <article class="">
@@ -39,9 +47,9 @@
{% else %} {% else %}
? ?
{% endif %} {% endif %}
<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 %} {% 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>
</p> </p>
</article> </article>
<article class=""> <article class="">
@@ -52,9 +60,9 @@
{% else %} {% else %}
? ?
{% endif %} {% endif %}
<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 %} {% 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>
</p> </p>
</article> </article>
<article class=""> <article class="">
@@ -65,9 +73,9 @@
{% else %} {% else %}
? ?
{% endif %} {% endif %}
<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 %} {% 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>
</p> </p>
</article> </article>
<article class=""> <article class="">
@@ -78,16 +86,17 @@
{% else %} {% else %}
? ?
{% endif %} {% endif %}
<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 %} {% 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>
</p> </p>
</article> </article>
{% endwith %} {% endwith %}
</div> </div>
<div class="card ~neutral !low py-0 mb-6"> <div class="card overflow-visible ~neutral !low py-0 mb-6">
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data %} {% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data tooltip_format=stats.session_chart_tooltip_format granularity=stats.session_chart_granularity click_zoom=True %}
</div> </div>
{% endif %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6"> <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="card ~neutral !low limited-height py-2"> <div class="card ~neutral !low limited-height py-2">
<table class="table"> <table class="table">
@@ -100,7 +109,7 @@
<tbody> <tbody>
{% for location in stats.locations %} {% for location in stats.locations %}
<tr> <tr>
<td>{{location.location|default:"Unknown"|urldisplay}}</td> <td class="truncate w-full max-w-0">{{location.location|default:"Unknown"|urldisplay}}</td>
<td class="rf">{{location.count|intcomma}}</td> <td class="rf">{{location.count|intcomma}}</td>
</tr> </tr>
{% empty %} {% empty %}
@@ -122,7 +131,7 @@
<tbody> <tbody>
{% for referrer in stats.referrers %} {% for referrer in stats.referrers %}
<tr> <tr>
<td>{{referrer.referrer|default:"Direct"|urldisplay}}</td> <td class="truncate w-full max-w-0">{{referrer.referrer|default:"Direct"|urldisplay}}</td>
<td class="rf">{{referrer.count|intcomma}}</td> <td class="rf">{{referrer.count|intcomma}}</td>
</tr> </tr>
{% empty %} {% empty %}
@@ -144,7 +153,9 @@
<tbody> <tbody>
{% for country in stats.countries %} {% for country in stats.countries %}
<tr> <tr>
<td>{{country.country|flag_emoji}} {{country.country|country_name}}</td> <td class="truncate w-full max-w-0" title="{{country.country|country_name}}">
<span class="{{country.country|flag_class}}"></span> {{country.country|country_name}}
</td>
<td class="rf">{{country.count|intcomma}}</td> <td class="rf">{{country.count|intcomma}}</td>
</tr> </tr>
{% empty %} {% empty %}
@@ -166,7 +177,9 @@
<tbody> <tbody>
{% for os in stats.operating_systems %} {% for os in stats.operating_systems %}
<tr> <tr>
<td>{{os.os|default:"Unknown"}}</td> <td class="flex items-center truncate w-full max-w-0" title="{{os.os|default:'Unknown'}}">
{{os.os|iconify}}<span>{{os.os|default:"Unknown"}}</span>
</td>
<td class="rf">{{os.count|intcomma}}</td> <td class="rf">{{os.count|intcomma}}</td>
</tr> </tr>
{% empty %} {% empty %}
@@ -188,7 +201,8 @@
<tbody> <tbody>
{% for browser in stats.browsers %} {% for browser in stats.browsers %}
<tr> <tr>
<td>{{browser.browser|default:"Unknown"}}</td> <td class="flex items-center truncate w-full max-w-0" title="{{browser.browser|default:'Unknown'}}">
{{browser.browser|iconify}}<span>{{browser.browser|default:"Unknown"}}</span></td>
<td class="rf">{{browser.count|intcomma}}</td> <td class="rf">{{browser.count|intcomma}}</td>
</tr> </tr>
{% empty %} {% empty %}
@@ -210,7 +224,7 @@
<tbody> <tbody>
{% for device_type in stats.device_types %} {% for device_type in stats.device_types %}
<tr> <tr>
<td>{{device_type.device_type|default:"Unknown"|title}}</td> <td class="truncate w-full max-w-0">{{device_type.device_type|default:"Unknown"|title}}</td>
<td class="rf">{{device_type.count|intcomma}}</td> <td class="rf">{{device_type.count|intcomma}}</td>
</tr> </tr>
{% empty %} {% empty %}
@@ -224,8 +238,9 @@
</div> </div>
<div class="card ~neutral !low limited-height py-2"> <div class="card ~neutral !low limited-height py-2">
{% include 'dashboard/includes/session_list.html' %} {% include 'dashboard/includes/session_list.html' %}
<hr class="sep h-8"> <hr class="sep h-8 md:h-12">
<a href="{% url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more sessions <a href="{% contextual_url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more
sessions
&rarr;</a> &rarr;</a>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -5,7 +5,7 @@
{% block head_title %}{{object.name}} Session{% endblock %} {% block head_title %}{{object.name}} Session{% endblock %}
{% block service_actions %} {% block service_actions %}
<a href="{% url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">Analytics &rarr;</a> <a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">Analytics &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% 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> {{session.last_seen|date:"g:i a"}}{% if session.is_currently_active %} <span class="chip ~positive !high text-base">Online</span>{% endif %}</p>
</div> </div>
</div> </div>
<hr class="sep h-8"> <hr class="sep h-8 md:h-12">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-gray-400 font-medium"> <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-gray-400 font-medium">
<div> <div>
<p>Browser</p> <p>Browser</p>
@@ -45,13 +45,13 @@
</div> </div>
<div> <div>
<p>Country</p> <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>
<div> <div>
<p>Location</p> <p>Location</p>
<p class="label"> <p class="label">
{% if session.latitude %} {% if session.latitude %}
<a href="https://www.google.com/maps/search/?api=1&query={{session.latitude}},{{session.longitude}}">Open <a href="{{session|location_url}}" target="_blank">Open
in Maps &nearr;</a> in Maps &nearr;</a>
{% else %} {% else %}
Unknown Unknown
@@ -72,7 +72,7 @@
</div> </div>
<div class="md:flex card ~neutral !low flex-grow justify-between"> <div class="md:flex card ~neutral !low flex-grow justify-between">
<div class="mb-4 md:mb-0 md:w-1/2"> <div class="mb-4 md:mb-0 md:w-1/2">
<p class="label font-medium text-lg">{{hit.location|default:"Unknown"|urlize}}</p> <p class="label font-medium text-lg truncate">{{hit.location|default:"Unknown"|urlize}}</p>
{% if hit.referrer %} {% if hit.referrer %}
<p>via {{hit.referrer|urlize}}<p> <p>via {{hit.referrer|urlize}}<p>
{% endif %} {% endif %}

View File

@@ -6,11 +6,11 @@
{% block service_actions %} {% block service_actions %}
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div> <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 &rarr;</a> <a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button field ~neutral bg-neutral-000 w-auto">Analytics &rarr;</a>
{% endblock %} {% endblock %}
{% block service_content %} {% block service_content %}
<div class="card ~neutral !low mb-8 pt-2 max-w-full overflow-x-scroll"> <div class="card ~neutral !low mb-8 pt-2 max-w-full overflow-x-auto">
{% include 'dashboard/includes/session_list.html' %} {% include 'dashboard/includes/session_list.html' %}
</div> </div>
{% pagination page_obj request %} {% pagination page_obj request %}

View File

@@ -12,12 +12,7 @@
<div class="max-w-xl content"> <div class="max-w-xl content">
<h5>Installation</h5> <h5>Installation</h5>
<p>Place the following snippet at the end of the <code>&lt;body&gt;</code> tag on any page you'd like to track.</p> <p>Place the following snippet at the end of the <code>&lt;body&gt;</code> tag on any page you'd like to track.</p>
<div class="card ~neutral !high font-mono text-sm"> {% include 'dashboard/includes/service_snippet.html' %}
{% 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>
<hr class="sep h-4"> <hr class="sep h-4">
<h5>Settings</h5> <h5>Settings</h5>
<form class="card ~neutral !low p-0" method="POST"> <form class="card ~neutral !low p-0" method="POST">
@@ -36,4 +31,4 @@
</div> </div>
</form> </form>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -6,20 +6,21 @@
{% block content %} {% block content %}
<div class="md:flex justify-between items-center" id="heading"> <div class="md:flex justify-between items-center" id="heading">
<a class="flex items-center mb-4 md:mb-0" href="{% url 'dashboard:service' object.uuid %}"> <a class="flex items-center mb-4 md:mb-0 truncate" href="{% contextual_url 'dashboard:service' object.uuid %}">
<h3 class="heading leading-none mr-4"> <h3 class="heading items-center mr-4 md:mr-2 flex truncate">
{{object.name}} {{object.link|iconify}}
<span class="flex-1 truncate ml-2" title="{{object.name}}">{{object.name}}</span>
</h3> </h3>
<div class='text-3xl'> <div class="text-3xl md:mr-2">
{% include 'dashboard/includes/stats_status_chip.html' %} {% include 'dashboard/includes/stats_status_chip.html' %}
</div> </div>
</a> </a>
<div class="flex items-center"> <div class="flex items-center flex-none">
{% block service_actions %} {% block service_actions %}
{% endblock %} {% endblock %}
</div> </div>
</div> </div>
<hr class="sep h-8"> <hr class="sep h-8 md:h-12">
{% block service_content %} {% block service_content %}
{% endblock %} {% endblock %}
{% endblock %} {% endblock %}

View File

@@ -1,12 +1,14 @@
from datetime import timedelta
from urllib.parse import urlparse from urllib.parse import urlparse
import urllib
import flag
import pycountry import pycountry
from django import template from django import template
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.html import escape from django.utils.html import escape
from django.utils.safestring import SafeString from django.utils.safestring import SafeString
from django.template.defaulttags import url as url_tag
register = template.Library() register = template.Library()
@@ -26,11 +28,11 @@ def naturaldelta(timedelta):
@register.filter @register.filter
def flag_emoji(isocode): def flag_class(isocode):
try: if isocode:
return flag.flag(isocode) return "mr-1 flag-icon flag-icon-" + isocode.lower()
except: else:
return "" return "hidden"
@register.filter @register.filter
@@ -43,7 +45,12 @@ def country_name(isocode):
@register.simple_tag @register.simple_tag
def relative_stat_tone( 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" good_classes = good_classes or "~positive"
bad_classes = bad_classes or "~critical" bad_classes = bad_classes or "~critical"
@@ -73,7 +80,7 @@ def percent_change_display(start, end):
elif start == 0: elif start == 0:
pct_change = "0%" pct_change = "0%"
else: else:
change = int(round(100 * abs(end - start) / start)) change = int(round(100 * abs(end - start) / max(start, 1)))
if change > 999: if change > 999:
return "> 999%" return "> 999%"
else: else:
@@ -84,8 +91,7 @@ def percent_change_display(start, end):
@register.inclusion_tag("dashboard/includes/sidebar_footer.html") @register.inclusion_tag("dashboard/includes/sidebar_footer.html")
def sidebar_footer(): def sidebar_footer():
return {"version": "" if settings.SHOW_SHYNET_VERSION return {"version": settings.VERSION if settings.SHOW_SHYNET_VERSION else ""}
else settings.VERSION}
@register.inclusion_tag("dashboard/includes/stat_comparison.html") @register.inclusion_tag("dashboard/includes/stat_comparison.html")
@@ -98,6 +104,12 @@ def compare(
bad_classes=None, bad_classes=None,
neutral_classes=None, neutral_classes=None,
): ):
if isinstance(start, timedelta):
start = start.seconds
if isinstance(end, timedelta):
end = end.seconds
return { return {
"start": start, "start": start,
"end": end, "end": end,
@@ -116,12 +128,103 @@ def startswith(text, starts):
return False 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 @register.filter
def urldisplay(url): def urldisplay(url):
if url.startswith("http"): if url.startswith("http"):
display_url = url.replace("http://", "").replace("https://", "") display_url = url.replace("http://", "").replace("https://", "")
return SafeString( return SafeString(
f"<a href='{url}' title='{url}' rel='nofollow'>{escape(display_url if len(display_url) < 40 else display_url[:40] + '...')}</a>" f"<a href='{url}' title='{url}' rel='nofollow' class='flex items-center mr-1'>{iconify(url)}<span class='truncate'>{escape(display_url)}</span></a>"
) )
else: else:
return url return url
class ContextualURLNode(template.Node):
"""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))

View File

@@ -22,16 +22,24 @@ from .forms import ServiceForm
from .mixins import DateRangeMixin from .mixins import DateRangeMixin
class DashboardView(LoginRequiredMixin, DateRangeMixin, TemplateView): class DashboardView(LoginRequiredMixin, DateRangeMixin, ListView):
model = Service
template_name = "dashboard/pages/dashboard.html" 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): def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
data["services"] = Service.objects.filter(
Q(owner=self.request.user) | Q(collaborators__in=[self.request.user]) for service in data["object_list"]:
).distinct() service.stats = service.get_core_stats(
for service in data["services"]: self.get_start_date(), self.get_end_date()
service.stats = service.get_core_stats(data["start_date"], data["end_date"]) )
return data return data
@@ -58,6 +66,7 @@ class ServiceView(
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
data = super().get_context_data(**kwargs) data = super().get_context_data(**kwargs)
data["script_protocol"] = "https://" if settings.SCRIPT_USE_HTTPS else "http://"
data["stats"] = self.object.get_core_stats(data["start_date"], data["end_date"]) data["stats"] = self.object.get_core_stats(data["start_date"], data["end_date"])
data["object_list"] = Session.objects.filter( data["object_list"] = Session.objects.filter(
service=self.get_object(), service=self.get_object(),

View File

@@ -18,7 +18,7 @@ import urllib.parse as urlparse
from django.contrib.messages import constants as messages from django.contrib.messages import constants as messages
# Increment on new releases # Increment on new releases
VERSION = "v0.6.7" VERSION = "v0.9.1"
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -58,6 +58,7 @@ INSTALLED_APPS = [
"allauth", "allauth",
"allauth.account", "allauth.account",
"allauth.socialaccount", "allauth.socialaccount",
"debug_toolbar",
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@@ -70,6 +71,7 @@ MIDDLEWARE = [
"django.contrib.sites.middleware.CurrentSiteMiddleware", "django.contrib.sites.middleware.CurrentSiteMiddleware",
"django.contrib.messages.middleware.MessageMiddleware", "django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
] ]
ROOT_URLCONF = "shynet.urls" ROOT_URLCONF = "shynet.urls"
@@ -145,9 +147,15 @@ AUTH_PASSWORD_VALIDATORS = [
{ {
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", "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.MinimumLengthValidator",
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",}, },
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
] ]
# Logging # Logging
@@ -247,6 +255,10 @@ LOGIN_REDIRECT_URL = "/"
SITE_ID = 1 SITE_ID = 1
INTERNAL_IPS = [
"127.0.0.1",
]
# Celery # Celery
CELERY_TASK_ALWAYS_EAGER = os.getenv("CELERY_TASK_ALWAYS_EAGER", "True") == "True" CELERY_TASK_ALWAYS_EAGER = os.getenv("CELERY_TASK_ALWAYS_EAGER", "True") == "True"
@@ -289,11 +301,16 @@ NPM_ROOT_PATH = "../"
NPM_FILE_PATTERNS = { NPM_FILE_PATTERNS = {
"a17t": [os.path.join("dist", "a17t.css"), os.path.join("dist", "tailwind.css")], "a17t": [os.path.join("dist", "a17t.css"), os.path.join("dist", "tailwind.css")],
"apexcharts": [os.path.join("dist", "apexcharts.min.js")], "apexcharts": [os.path.join("dist", "apexcharts.min.js")],
"litepicker": [os.path.join("dist", "js", "main.js")], "litepicker": [
os.path.join("dist", "nocss", "litepicker.js"),
os.path.join("dist", "css", "litepicker.css"),
os.path.join("dist", "plugins", "ranges.js"),
],
"turbolinks": [os.path.join("dist", "turbolinks.js")], "turbolinks": [os.path.join("dist", "turbolinks.js")],
"stimulus": [os.path.join("dist", "stimulus.umd.js")], "stimulus": [os.path.join("dist", "stimulus.umd.js")],
"inter-ui": [os.path.join("Inter (web)", "*")], "inter-ui": [os.path.join("Inter (web)", "*")],
"@fortawesome": [os.path.join("fontawesome-free", "js", "all.min.js")], "@fortawesome": [os.path.join("fontawesome-free", "js", "all.min.js")],
"flag-icon-css": [os.path.join("css", "flag-icon.min.css"), os.path.join("flags", "*")],
} }
# Shynet # Shynet
@@ -318,3 +335,18 @@ SESSION_MEMORY_TIMEOUT = int(os.getenv("SESSION_MEMORY_TIMEOUT", "1800"))
# Should the Shynet version information be displayed? # Should the Shynet version information be displayed?
SHOW_SHYNET_VERSION = os.getenv("SHOW_SHYNET_VERSION", "True") == "True" 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"))

View File

@@ -15,8 +15,10 @@ Including another URLconf
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
import debug_toolbar
urlpatterns = [ urlpatterns = [
path("__debug__/", include(debug_toolbar.urls)),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("accounts/", include("allauth.urls")), path("accounts/", include("allauth.urls")),
path("ingress/", include(("analytics.ingress_urls", "ingress")), name="ingress"), path("ingress/", include(("analytics.ingress_urls", "ingress")), name="ingress"),

View File

@@ -3,11 +3,14 @@
<html> <html>
<head> <head>
<title>Pixel test</title> <title>JS test</title>
</head> </head>
<body> <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> </body>
</html> </html>