Compare commits

..

161 Commits

Author SHA1 Message Date
R. Miles McCain
c718d49c54 Use Alpine 3.14 2021-12-31 13:26:44 -05:00
R. Miles McCain
04f095b4fb Use new a17t and Tailwind 2021-12-31 13:25:10 -05:00
R. Miles McCain
fc38fd88bd Use new a17t/Tailwind 2021-12-31 13:02:16 -05:00
R. Miles McCain
7d92a557f4 Use latest alpine 2021-12-31 12:10:19 -05:00
R. Miles McCain
e12848b094 Lessen priorities on field buttons 2021-12-21 00:58:11 -05:00
R. Miles McCain
e534269c77 Provide initial contributing guide 2021-12-21 00:23:29 -05:00
R. Miles McCain
0d64ef33b0 Remove installation with SSL from guide TOC 2021-12-21 00:15:15 -05:00
R. Miles McCain
56c82e7d23 Remove HTTPS without reverse proxy from the GUIDE 2021-12-21 00:14:18 -05:00
R. Miles McCain
c71d934c67 Remove armv7 support 2021-12-21 00:10:12 -05:00
R. Miles McCain
85ae56fcdb Add swap space to GitHub Actions runners 2021-12-18 15:34:11 -05:00
R. Miles McCain
cd422ffd71 Add note about ALLOWED_HOSTS to GUIDE.md 2021-12-18 15:14:11 -05:00
R. Miles McCain
060a9b2a96 Update Python dependencies 2021-12-18 14:38:31 -05:00
R. Miles McCain
8d13ccd0fd Update dependencies 2021-12-18 13:56:57 -05:00
lionep
0d46e6d1f4 Fix typo in GUIDE.md (#180) 2021-12-04 09:50:26 -08:00
R. Miles McCain
81ae84efb3 Add office hours link 2021-11-21 00:12:24 -08:00
R. Miles McCain
ea5f58fbd3 Update lock 2021-11-13 21:11:52 -08:00
dependabot[bot]
4079a8494a Bump sqlparse from 0.4.1 to 0.4.2 (#178)
Bumps [sqlparse](https://github.com/andialbrecht/sqlparse) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/andialbrecht/sqlparse/releases)
- [Changelog](https://github.com/andialbrecht/sqlparse/blob/master/CHANGELOG)
- [Commits](https://github.com/andialbrecht/sqlparse/compare/0.4.1...0.4.2)

---
updated-dependencies:
- dependency-name: sqlparse
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-13 21:10:06 -08:00
Sérgio
780b71083a Add first factories and first dashboard tests (#172)
* Add factories and first dashboard tests

* Code cleanup

Co-authored-by: R. Miles McCain <github@sendmiles.email>
2021-11-13 21:09:55 -08:00
Sérgio
62fbb014e7 Testing setup (dependencies and github actions) (#169)
* Add testing dependencies

* Add draft github action

* Fix testing env variables

* Newline lint

* Run tests on every pull request and push
2021-11-13 21:03:06 -08:00
JuniorJPDJ
53bc690435 fix(docker): healthcheck (#175)
fixes healthcheck for $ALLOWED_HOSTS longer than 2 domains
2021-09-30 16:12:57 -07:00
JuniorJPDJ
04120323a6 feat(docker): healthcheck (#166) 2021-09-16 13:35:38 -07:00
R. Miles McCain
03ced00f63 Remove Pipfile 2021-07-31 23:54:08 +00:00
R. Miles McCain
8d04ed5c1f Always install debug toolbar 2021-07-31 23:53:41 +00:00
R. Miles McCain
f2879775ef Set default auto field 2021-07-31 23:49:01 +00:00
R. Miles McCain
c980567fee Formatting 2021-07-31 23:44:05 +00:00
R. Miles McCain
57c8695bcc Debug toolbar fix 2021-07-31 23:44:03 +00:00
R. Miles McCain
31ffa47fd3 Bump version 2021-07-20 13:37:37 +00:00
R. Miles McCain
73f3513dfe Use Poetry, not Pipenv 2021-07-20 04:51:13 +00:00
R. Miles McCain
b2e9d50d78 Update psycopg2 2021-07-20 03:05:18 +00:00
Casper Verswijvelt
de235c02a7 Add ability to toggle between map chart and country/session table (#153)
* Add ability to toggle between geo map and table view

* Add back haaavk's bar visualisation for countries table

* Change text/location of map/table toggle buttons

* Add some common css to reusable class

* CSS consistency

* Use button, not span for interactive elements

Co-authored-by: R. Miles McCain <oci@sendmiles.email>
2021-07-19 23:04:56 -04:00
Kasper Seweryn
31cb616242 Change snippet url to display current host (#159)
* Change snippet url to current host

* Change site.domain to host in page.js

* Remove useless condition

* Change hostname in email messages

* Remove `hostname` command

* Fix startup_checks.sh

* Remove unused variable from startup_checks.py
2021-07-19 22:55:30 -04:00
havk
2d5fbae279 Add sessions key to hits_per_day dict (#160) 2021-07-19 22:38:45 -04:00
Casper Verswijvelt
0153b1f847 Fix ellipsis in multiple tables (#152) 2021-06-15 15:29:59 -04:00
R. Miles McCain
473ad93081 Properly name manual build action 2021-06-14 20:56:29 +00:00
R. Miles McCain
1225ad90e8 Add manual build option
Signed-off-by: R. Miles McCain <github@sendmiles.email>
2021-06-14 20:55:22 +00:00
R. Miles McCain
e43718f596 Bump version 2021-06-13 20:11:28 +00:00
R. Miles McCain
d9623a9905 Also build image on push to master 2021-06-13 20:08:45 +00:00
R. Miles McCain
011f1f13c8 Also build image on push to master 2021-06-13 19:58:15 +00:00
Casper Verswijvelt
9832de0c19 GeoIP Map (#142)
* First working version of world map chart

* Cleanup code, fix aspect ratio, add GeoIP Map header

* Remove limited-height on session list with already limited content

* Update package lock

* Integrate map into service page

* Adjust map colors

* Adjust colors further

Co-authored-by: R. Miles McCain <github@sendmiles.email>
2021-06-13 15:50:01 -04:00
havk
83b20643d2 Add hits to chart data (#141)
* Add hits to chart data

* Hide legend

Co-authored-by: R. Miles McCain <github@sendmiles.email>
2021-06-13 14:39:45 -04:00
R. Miles McCain
ab44ba8318 Add subtle rounding to bar 2021-06-13 18:32:55 +00:00
havk
fcea6d3be9 Percents + visualization (#139)
* Black autoformat

* Add percent and divide filters

* Remove divide filter

* Add percents in brackets and visualization

* Apply percents and visualization to all data

* Switch absolute to relative

* Increase percent bar height

* Move bar to separated file

* Add USE_RELATIVE_MAX_IN_BAR_VISUALIZATION to settings

* Add flex items-center

* Move bar to left

* Remove spaces

* Fix USE_RELATIVE_MAX_IN_BAR_VISUALIZATION

* Remove unnecessary True

* Add bar_width tag

* Add flex-none to make flag not get squished

* Fix flex-none
2021-06-13 14:11:40 -04:00
Ian MacIntosh
f3a89bff78 Document possible time zone values (#147)
This might be a normal expectation, but it was new for me.

I added a URL to fast-track the next person in the same position.
2021-06-13 13:58:13 -04:00
dependabot[bot]
3c9bc9f3c9 Bump django from 3.1.9 to 3.1.12 (#150)
Bumps [django](https://github.com/django/django) from 3.1.9 to 3.1.12.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.9...3.1.12)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-13 13:50:48 -04:00
dependabot[bot]
2f5d0ba7e5 Bump django from 3.1.8 to 3.1.9 (#146)
Bumps [django](https://github.com/django/django) from 3.1.8 to 3.1.9.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/3.1.8...3.1.9)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-04 22:32:57 -04:00
dependabot[bot]
1c866209c9 Bump urllib3 from 1.26.4 to 1.26.5 (#145)
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.4 to 1.26.5.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.4...1.26.5)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: indirect
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-06-02 11:27:14 -04:00
Casper Verswijvelt
a4785b1a0c Datepicker mobile overflow (#143)
* Remove w-auto to fix datepicker overflow on mobile

* Hardcoded width for datepicker input element
2021-05-23 12:14:03 -04:00
R. Miles McCain
2928e663db Move sample Kubernetes deployment to latest 2021-05-14 17:01:15 +00:00
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
89 changed files with 6351 additions and 815 deletions

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

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

View File

@@ -0,0 +1,48 @@
name: Build manual Docker images
on:
workflow_dispatch:
inputs:
tag:
description: 'Docker image tag'
jobs:
publish_to_docker_hub:
runs-on: ubuntu-latest
steps:
- name: Set swap space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 5
- name: Checkout code
uses: actions/checkout@v2
- name: Prepare tags
id: prep
run: |
DOCKER_IMAGE=milesmcc/shynet
TAGS="${DOCKER_IMAGE}:${{ github.event.inputs.tag }}"
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
push: true
tags: ${{ steps.prep.outputs.tags }}

View File

@@ -0,0 +1,49 @@
name: Build release Docker images
on:
push:
tags:
- "*"
jobs:
publish_to_docker_hub:
runs-on: ubuntu-latest
steps:
- name: Set swap space
uses: pierotofy/set-swap-space@master
with:
swap-size-gb: 5
- 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
push: true
tags: ${{ steps.prep.outputs.tags }}

37
.github/workflows/run-tests.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: Run tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
db:
image: postgres:12.3-alpine
env:
POSTGRES_USER: shynet_db_user
POSTGRES_PASSWORD: shynet_db_user_password
POSTGRES_DB: shynet_db
ports:
- 5432:5432
strategy:
max-parallel: 4
matrix:
python-version: [3.9]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Run image
uses: abatilo/actions-poetry@v2.0.0
with:
poetry-version: 1.1.6
- name: Install dependencies
run: poetry install
- name: Django Testing project
run: |
cp TEMPLATE.env .env
poetry run ./shynet/manage.py test

5
.gitignore vendored
View File

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

15
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,15 @@
# Contributing
This document provides an overview of how to contribute to Shynet. Currently, it focuses on the more technical elements of contributing --- for example, setting up your development environment. Eventually, we will expand this guide to cover the social and governance oriented side of contributing as well.
## Setting up your development environment
To contribute to Shynet, you must have a reliable development environment. Because Shynet is intended to be run inside containers, we strongly encourage you to run Shynet in a container in development as well. The development setup described in this guide will use Docker and Docker Compose.
To begin, clone the Shynet repository to your computer, and ensure that you have Docker and Docker Compose installed.
Copy `TEMPLATE.env` to a new file called `.env`. This `.env` file will be used in your development environment. Paste `DEBUG=True` into the end of your new `.env` file so that Shynet will know to run in development mode.
Finally, follow the steps in [GUIDE.md](GUIDE.md) on setting up a Shynet instance with Docker Compose. This is where you'll setup an admin user.
_Did you have to perform additional steps to setup your environment? Document them here and submit a pull request!_

View File

@@ -1,26 +1,39 @@
FROM python:3-alpine
FROM python:alpine3.14
# Getting things ready
WORKDIR /usr/src/shynet
COPY Pipfile.lock Pipfile ./
COPY package.json package-lock.json ../
# Django expects node_modules to be in its parent directory.
# Install dependencies & configure machine
ARG GF_UID="500"
ARG GF_GID="500"
RUN apk update && \
apk add gettext curl bash npm && \
curl -m 180 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=kKG1ebhL3iWVd0iv&suffix=tar.gz" | tar -xvz -C /tmp && \
apk add gettext curl bash npm libffi-dev rust cargo
# libffi-dev and rust are used for the cryptography package,
# which we indirectly rely on. Necessary for aarch64 support.
# Collect GeoIP Database
RUN curl -m 180 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=kKG1ebhL3iWVd0iv&suffix=tar.gz" | tar -xvz -C /tmp && \
curl -m 180 "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=kKG1ebhL3iWVd0iv&suffix=tar.gz" | tar -xvz -C /tmp && \
mv /tmp/GeoLite2*/*.mmdb /etc && \
apk del curl && \
apk add --no-cache postgresql-libs && \
apk del curl
# Move dependency files
COPY poetry.lock pyproject.toml ./
COPY package.json package-lock.json ../
# Django expects node_modules to be in its parent directory.
# Install more dependencies
RUN apk add --no-cache postgresql-libs && \
apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev && \
npm i -P --prefix .. && \
pip install pipenv~=2020.6.2 && \
pipenv install --system --deploy && \
apk --purge del .build-deps && \
pip install poetry==1.1.7
# Install Python dependencies
RUN poetry config virtualenvs.create false && \
poetry install --no-dev --no-interaction --no-ansi
# Cleanup dependencies & setup user group
RUN apk --purge del .build-deps && \
rm -rf /var/lib/apt/lists/* && \
rm /var/cache/apk/* && \
addgroup --system -g $GF_GID appgroup && \
@@ -28,10 +41,15 @@ RUN apk update && \
# Install Shynet
COPY shynet .
# Build Tailwind CSS and build static files
COPY tailwind.config.js .
RUN npx tailwindcss -i ./a17t/static/a17t/css/tailwind.css -o ./a17t/static/a17t/dist/tailwind.css --jit --minify
RUN python manage.py collectstatic --noinput && \
python manage.py compilemessages
# Launch
USER appuser
EXPOSE 8080
HEALTHCHECK CMD bash -c 'wget -o /dev/null -O /dev/null --header "Host: ${ALLOWED_HOSTS%%,*}" "http://127.0.0.1:$PORT/healthz/?format=json"'
CMD [ "./entrypoint.sh" ]

View File

@@ -7,7 +7,6 @@
- [Render](#render)
- [Updating Your Configuration](#updating-your-configuration)
- [Advanced Usage](#advanced-usage)
* [Installation with SSL](#installation-with-ssl)
* [Configuring a Reverse Proxy](#configuring-a-reverse-proxy)
+ [Cloudflare](#cloudflare)
+ [Nginx](#nginx)
@@ -23,7 +22,7 @@
## Installation
Installation of Shynet is easy! Follow the [Basic Installation](#basic-installation) guide or the [Basic Installation with Docker Compose](#basic-installation-with-docker-compose) below for a minimal installation, or if you are going to be running Shynet over HTTPS through a reverse proxy. If you'd like to run Shynet over HTTPS without a reverse proxy, skip ahead to [Installation with SSL](#installation-with-ssl) instead.
Installation of Shynet is easy! Follow the [Basic Installation](#basic-installation) guide or the [Basic Installation with Docker Compose](#basic-installation-with-docker-compose) below for a minimal installation, or if you are going to be running Shynet over HTTPS through a reverse proxy.
> **These commands assume Ubuntu.** If you're installing Shynet on a different platform, the process will be different.
@@ -35,21 +34,19 @@ Before continuing, please be sure to have the latest version of Docker installed
2. Have a PostgreSQL server ready to go. This can be on the same machine as the deployment, or elsewhere. You'll just need a username, password, host, and port. (For info on how to setup a PostgreSQL server on Ubuntu, follow [this guide](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-18-04)).
3. Configure an environment file for Shynet, using [this file](/TEMPLATE.env) as a template. (This file is typically named `.env`.) Make sure you set the database settings, or Shynet won't be able to run.
3. Configure an environment file for Shynet, using [this file](/TEMPLATE.env) as a template. (This file is typically named `.env`.) Make sure you set the database settings, or Shynet won't be able to run. Also consider setting `ALLOWED_HOSTS` inside the environment file to your deployment's domain for better security.
4. Launch the Shynet server for the first time by running `docker run --env-file=<your env file> milesmcc/shynet:latest`. Provided you're using the default environment information (i.e., `PERFORM_CHECKS_AND_SETUP` is `True`), you'll see a few warnings about not having an admin user or host setup; these are normal. Don't worry — we'll do this in the next step. You only need to stop if you see a stacktrace about being unable to connect to the database.
5. Create an admin user by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py registeradmin <your email>`. A temporary password will be printed to the console.
6. Set the hostname of your Shynet instance by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py hostname <your public hostname>`, where `<your public hostname>` is the _publicly accessible hostname_ of your instance, including port. This setting affects the URL that the tracking script sends its results to, so make sure it's correct. (Example hostnames: `shynet.example.com` or `example.com:8000`.)
6. Set the whitelabel of your Shynet instance by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py whitelabel <whitelabel>`. While this setting doesn't affect any core operations of Shynet, it lets you rename Shynet to whatever you want. (Example whitelabels: `"My Shynet Instance"` or `"Acme Analytics"`.)
7. Set the whitelabel of your Shynet instance by running `docker run --env-file=<your env file> milesmcc/shynet:latest ./manage.py whitelabel <whitelabel>`. While this setting doesn't affect any core operations of Shynet, it lets you rename Shynet to whatever you want. (Example whitelabels: `"My Shynet Instance"` or `"Acme Analytics"`.)
7. Launch your webserver by running `docker run --env-file=<your env file> milesmcc/shynet:latest`. You may need to bind Docker's port 8080 (where Shynet runs) to your local port 80 (http); this can be done using the flag `-p 80:8080` after `run`. Visit your service's homepage, and verify everything looks right! You should see a login prompt. Log in with the credentials from step 5. You'll probably be prompted to "confirm your email"—if you haven't set up an email server, the confirmation email will be printed to the console instead.
8. Launch your webserver by running `docker run --env-file=<your env file> milesmcc/shynet:latest`. You may need to bind Docker's port 8080 (where Shynet runs) to your local port 80 (http); this can be done using the flag `-p 80:8080` after `run`. Visit your service's homepage, and verify everything looks right! You should see a login prompt. Log in with the credentials from step 5. You'll probably be prompted to "confirm your email"—if you haven't set up an email server, the confirmation email will be printed to the console instead.
8. Create a service by clicking "+ Create Service" in the top right hand corner. Fill out the options as appropriate. Once you're done, press "create" and you'll be redirected to your new service's analytics page.
9. Create a service by clicking "+ Create Service" in the top right hand corner. Fill out the options as appropriate. Once you're done, press "create" and you'll be redirected to your new service's analytics page.
10. Finally, click on "Manage" in the top right of the service's page to get the tracking script code. Inject this script on all pages you'd like the service to track.
9. Finally, click on "Manage" in the top right of the service's page to get the tracking script code. Inject this script on all pages you'd like the service to track.
### Basic Installation with Docker Compose
@@ -58,7 +55,7 @@ Before continuing, please be sure to have the latest version of Docker installed
1. Clone the repository.
2. Using [TEMPLATE.env](/TEMPLATE.env) as a template, confiure the environment for your Shynet instance and place the modified config in a file called `.env` in the root of the repository. Do _not_ change the port number at the end; you can set the public facing port in the next step.
2. Using [TEMPLATE.env](/TEMPLATE.env) as a template, configure the environment for your Shynet instance and place the modified config in a file called `.env` in the root of the repository. Do _not_ change the port number at the end; you can set the public facing port in the next step.
3. On line 2 of the `nginx.conf` file located in the root of the repository, replace `example.com` with your hostname. Then, in the `docker-compose.yml` file, set the port number by replacing `8080` in line 38 ( `- 8080:80` ) with whatever local port you want to bind it to. For example, set the port number to `- 80:80` if you want your site will be available via HTTP (port 80) at `http://<your hostname>`.
@@ -66,9 +63,7 @@ Before continuing, please be sure to have the latest version of Docker installed
5. Create an admin user by running `docker exec -it shynet_main ./manage.py registeradmin <your email>`. A temporary password will be printed to the console.
6. Set the hostname of your Shynet instance by running `docker exec -it shynet_main ./manage.py hostname <your public hostname>`, where `<your public hostname>` is the same as the hostname you set in step 3. This setting affects the URL that the tracking script sends its results to, so make sure it's correct. (Example hostnames: shynet.example.com or example.com:8000.)
7. Set the whitelabel of your Shynet instance by running `docker exec -it shynet_main ./manage.py whitelabel <whitelabel>`. While this setting doesn't affect any core operations of Shynet, it lets you rename Shynet to whatever you want. (Example whitelabels: "My Shynet Instance" or "Acme Analytics".)
6. Set the whitelabel of your Shynet instance by running `docker exec -it shynet_main ./manage.py whitelabel <whitelabel>`. While this setting doesn't affect any core operations of Shynet, it lets you rename Shynet to whatever you want. (Example whitelabels: "My Shynet Instance" or "Acme Analytics".)
Your site should now be accessible at `http://hostname:port`. Now you can follow steps 9-10 of the [Basic Installation](#basic-installation) guide above to get Shynet integrated on your sites.
@@ -78,11 +73,10 @@ You may wish to deploy Shynet on Heroku. Note that Heroku's free offerings (name
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/milesmcc/shynet/tree/master)
Once you deploy, you'll need to setup an admin user, whitelabel, and hostname before you can use Shynet. Do that with the following commands:
Once you deploy, you'll need to setup an admin user and whitelabel before you can use Shynet. Do that with the following commands:
1. `heroku run --app=<your app> ./manage.py registeradmin <your email>`
2. `heroku run --app=<your app> ./manage.py hostname <the hostname where you will run Shynet>`
3. `heroku run --app=<your app> ./manage.py whitelabel "<your Shynet instance's name>"`
2. `heroku run --app=<your app> ./manage.py whitelabel "<your Shynet instance's name>"`
## Render
@@ -93,8 +87,7 @@ Once you deploy, you'll need to setup an admin user, whitelabel, and hostname be
Once your deploy has completed, use the **Render Shell** to configure your app:
1. Set your email: `./manage.py registeradmin your-email@example.com`
1. Add your onrender.com domain: `./manage.py hostname your-shynet-domain.onrender.com`
1. Set your whitelabel: `./manage.py whitelabel "Your Shynet Instance Name"`
2. Set your whitelabel: `./manage.py whitelabel "Your Shynet Instance Name"`
See the [Render docs](https://render.com/docs/deploy-shynet) for more information on deploying your application on Render.
@@ -102,40 +95,6 @@ See the [Render docs](https://render.com/docs/deploy-shynet) for more informatio
## Advanced Usage
### Installation with SSL
If you are going to be running Shynet through a reverse proxy, please see [Configuring a Reverse Proxy](#configuring-a-reverse-proxy) instead.
0. We'll be cloning this into the home directory to make this installation easier, so run `cd ~/` if you need to.
1. Instead of pulling from Docker, we will be pulling from GitHub and building using Docker in order to easily add SSL certificates. You will want to run `git clone https://github.com/milesmcc/shynet.git` to clone the GitHub repo to your current working directory.
2. To install `certbot` follow [the guide here](https://certbot.eff.org/instructions) or follow along below
* Ubuntu 18.04
* `sudo apt-get update`
* `sudo apt-get install software-properties-common`
* `sudo add-apt-repository universe`
* `sudo add-apt-repository ppa:certbot/certbot`
* `sudo apt-get update`
* `sudo apt-get install certbot`
3. Run `sudo certbot certonly --standalone` and follow the instructions to generate your SSL certificate.
* If you registering the certificate to a domain name like `example.com`, please be sure to point your DNS records to your current server before running `certbot`.
4. We are going to move the SSL certificates to the Shynet repo with with command below. Replace `<domain>` with the domain name you used in step 3.
* `cp /etc/letsencrypt/live/<domain>/{cert,privkey}.pem ~/shynet/shynet/`
5. With that, we are going to replace the `webserver.sh` with `ssl.webserver.sh` to enable the use of SSL certificates. The original `webserver.sh` will be backed up to `backup.webserver.sh`
* `mv ~/shynet/shynet/webserver.sh ~/shynet/shynet/backup.webserver.sh`
* `mv ~/shynet/shynet/ssl.webserver.sh ~/shynet/shynet/webserver.sh`
6. Now we build the image!
* `docker image build shynet -t shynet-ssl:latest`
7. Have a PostgreSQL server ready to go. This can be on the same machine as the deployment, or elsewhere. You'll just need a username, password, host, and port (default is `5432`). (For info on how to setup a PostgreSQL server on Ubuntu, follow [this guide](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-18-04)).
8. Follow the [Basic Installation](#basic-installation) guide with just one modification: in step #4, change the local bind port from `80` to `443`, and use `shynet-ssl:latest` as your Docker image instead of `milesmcc/shynet:latest`.
### Configuring a Reverse Proxy
A reverse proxy has many benefits. It can be used for DDoS protection, caching files to reduce server load, routing HTTPS and/or HTTP connections, hosting multiple services on a single server, [and more](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/)!
@@ -272,4 +231,4 @@ Here are solutions for some common issues. If your situation isn't described her
#### Shynet can't connect to my database running on `localhost`/`127.0.0.1`
* The problem is likely that to Shynet, `localhost` points to the local network in the container itself, not on the host machine. Try adding the `--network='host'` option when you run Docker.
* The problem is likely that to Shynet, `localhost` points to the local network in the container itself, not on the host machine. Try adding the `--network='host'` option when you run Docker.

25
Pipfile
View File

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

327
Pipfile.lock generated
View File

@@ -1,327 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "73fbc4a0251ccae805550aa48b70ec9f668bee6fa4ff9503e023b3854a06bce8"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"amqp": {
"hashes": [
"sha256:70cdb10628468ff14e57ec2f751c7aa9e48e7e3651cfd62d431213c0c4e58f21",
"sha256:aa7f313fb887c91f15474c1229907a04dac0b8135822d6603437803424c0aa59"
],
"version": "==2.6.1"
},
"asgiref": {
"hashes": [
"sha256:5ee950735509d04eb673bd7f7120f8fa1c9e2df495394992c73234d526907e17",
"sha256:7162a3cb30ab0609f1a4c95938fd73e8604f63bdba516a7f7d64b83ff09478f0"
],
"version": "==3.3.1"
},
"billiard": {
"hashes": [
"sha256:bff575450859a6e0fbc2f9877d9b715b0bbc07c3565bb7ed2280526a0cdf5ede",
"sha256:d91725ce6425f33a97dfa72fb6bfef0e47d4652acd98a032bd1a7fbf06d5fa6a"
],
"version": "==3.6.3.0"
},
"celery": {
"hashes": [
"sha256:a92e1d56e650781fb747032a3997d16236d037c8199eacd5217d1a72893bca45",
"sha256:d220b13a8ed57c78149acf82c006785356071844afe0b27012a4991d44026f9f"
],
"index": "pypi",
"version": "==4.4.7"
},
"certifi": {
"hashes": [
"sha256:1f422849db327d534e3d0c5f02a263458c3955ec0aae4ff09b95f195c59f4edd",
"sha256:f05def092c44fbf25834a51509ef6e631dc19765ab8a57b4e7ab85531f0a9cf4"
],
"version": "==2020.11.8"
},
"chardet": {
"hashes": [
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
],
"version": "==3.0.4"
},
"defusedxml": {
"hashes": [
"sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93",
"sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5"
],
"version": "==0.6.0"
},
"django": {
"hashes": [
"sha256:14a4b7cd77297fba516fc0d92444cc2e2e388aa9de32d7a68d4a83d58f5a4927",
"sha256:14b87775ffedab2ef6299b73343d1b4b41e5d4e2aa58c6581f114dbec01e3f8f"
],
"index": "pypi",
"version": "==3.1.3"
},
"django-allauth": {
"hashes": [
"sha256:f17209410b7f87da0a84639fd79d3771b596a6d3fc1a8e48ce50dabc7f441d30"
],
"index": "pypi",
"version": "==0.42.0"
},
"django-health-check": {
"hashes": [
"sha256:2667b89b8f85ad9b2a24c90581b376016d22ea912fedf37f9866413a3c2e0a5d",
"sha256:894738bd7e461b2405c005927403ad5ee8048bbaf5934cf30b2c81a4e047d4b0"
],
"index": "pypi",
"version": "==3.12.3"
},
"django-ipware": {
"hashes": [
"sha256:a7c7a8fd019dbdc9c357e6e582f65034e897572fc79a7e467674efa8aef9d00b"
],
"index": "pypi",
"version": "==2.1.0"
},
"django-npm": {
"hashes": [
"sha256:2e6bba65e728fa18b9db3c8dc0d4490b70cb7f43bacf60eb3654d7dcb6424272"
],
"index": "pypi",
"version": "==1.0.0"
},
"django-redis-cache": {
"hashes": [
"sha256:9a2eebef421d996a82098a19d17ff6b321265cd73178fa398913019764e8394a"
],
"index": "pypi",
"version": "==3.0.0"
},
"emoji-country-flag": {
"hashes": [
"sha256:256e47d30fb43bf154f370cc3c9e767f003aeb8653e31ba7b87151669c608e19",
"sha256:3f6c32699c19489383497865b208260b1d55b8424d66e08049187b13db2f0b8a"
],
"index": "pypi",
"version": "==1.2.3"
},
"geoip2": {
"hashes": [
"sha256:5869e987bc54c0d707264fec4710661332cc38d2dca5a7f9bb5362d0308e2ce0",
"sha256:99ec12d2f1271a73a0a4a2b663fe6ce25fd02289c0a6bef05c0a1c3b30ee95a4"
],
"index": "pypi",
"version": "==3.0.0"
},
"gunicorn": {
"hashes": [
"sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
"sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
],
"index": "pypi",
"version": "==20.0.4"
},
"html2text": {
"hashes": [
"sha256:c7c629882da0cf377d66f073329ccf34a12ed2adf0169b9285ae4e63ef54c82b",
"sha256:e296318e16b059ddb97f7a8a1d6a5c1d7af4544049a01e261731d2d5cc277bbb"
],
"index": "pypi",
"version": "==2020.1.16"
},
"idna": {
"hashes": [
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
"sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
],
"version": "==2.10"
},
"kombu": {
"hashes": [
"sha256:be48cdffb54a2194d93ad6533d73f69408486483d189fe9f5990ee24255b0e0a",
"sha256:ca1b45faac8c0b18493d02a8571792f3c40291cf2bcf1f55afed3d8f3aa7ba74"
],
"version": "==4.6.11"
},
"maxminddb": {
"hashes": [
"sha256:47e86a084dd814fac88c99ea34ba3278a74bc9de5a25f4b815b608798747c7dc"
],
"version": "==2.0.3"
},
"oauthlib": {
"hashes": [
"sha256:bee41cc35fcca6e988463cacc3bcb8a96224f470ca547e697b604cc697b2f889",
"sha256:df884cd6cbe20e32633f1db1072e9356f53638e4361bef4e8b03c9127c9328ea"
],
"version": "==3.1.0"
},
"psycopg2-binary": {
"hashes": [
"sha256:0deac2af1a587ae12836aa07970f5cb91964f05a7c6cdb69d8425ff4c15d4e2c",
"sha256:0e4dc3d5996760104746e6cfcdb519d9d2cd27c738296525d5867ea695774e67",
"sha256:11b9c0ebce097180129e422379b824ae21c8f2a6596b159c7659e2e5a00e1aa0",
"sha256:15978a1fbd225583dd8cdaf37e67ccc278b5abecb4caf6b2d6b8e2b948e953f6",
"sha256:1fabed9ea2acc4efe4671b92c669a213db744d2af8a9fc5d69a8e9bc14b7a9db",
"sha256:2dac98e85565d5688e8ab7bdea5446674a83a3945a8f416ad0110018d1501b94",
"sha256:42ec1035841b389e8cc3692277a0bd81cdfe0b65d575a2c8862cec7a80e62e52",
"sha256:6422f2ff0919fd720195f64ffd8f924c1395d30f9a495f31e2392c2efafb5056",
"sha256:6a32f3a4cb2f6e1a0b15215f448e8ce2da192fd4ff35084d80d5e39da683e79b",
"sha256:7312e931b90fe14f925729cde58022f5d034241918a5c4f9797cac62f6b3a9dd",
"sha256:7d92a09b788cbb1aec325af5fcba9fed7203897bbd9269d5691bb1e3bce29550",
"sha256:833709a5c66ca52f1d21d41865a637223b368c0ee76ea54ca5bad6f2526c7679",
"sha256:89705f45ce07b2dfa806ee84439ec67c5d9a0ef20154e0e475e2b2ed392a5b83",
"sha256:8cd0fb36c7412996859cb4606a35969dd01f4ea34d9812a141cd920c3b18be77",
"sha256:950bc22bb56ee6ff142a2cb9ee980b571dd0912b0334aa3fe0fe3788d860bea2",
"sha256:a0c50db33c32594305b0ef9abc0cb7db13de7621d2cadf8392a1d9b3c437ef77",
"sha256:a0eb43a07386c3f1f1ebb4dc7aafb13f67188eab896e7397aa1ee95a9c884eb2",
"sha256:aaa4213c862f0ef00022751161df35804127b78adf4a2755b9f991a507e425fd",
"sha256:ac0c682111fbf404525dfc0f18a8b5f11be52657d4f96e9fcb75daf4f3984859",
"sha256:ad20d2eb875aaa1ea6d0f2916949f5c08a19c74d05b16ce6ebf6d24f2c9f75d1",
"sha256:b4afc542c0ac0db720cf516dd20c0846f71c248d2b3d21013aa0d4ef9c71ca25",
"sha256:b8a3715b3c4e604bcc94c90a825cd7f5635417453b253499664f784fc4da0152",
"sha256:ba28584e6bca48c59eecbf7efb1576ca214b47f05194646b081717fa628dfddf",
"sha256:ba381aec3a5dc29634f20692349d73f2d21f17653bda1decf0b52b11d694541f",
"sha256:bd1be66dde2b82f80afb9459fc618216753f67109b859a361cf7def5c7968729",
"sha256:c2507d796fca339c8fb03216364cca68d87e037c1f774977c8fc377627d01c71",
"sha256:cec7e622ebc545dbb4564e483dd20e4e404da17ae07e06f3e780b2dacd5cee66",
"sha256:d14b140a4439d816e3b1229a4a525df917d6ea22a0771a2a78332273fd9528a4",
"sha256:d1b4ab59e02d9008efe10ceabd0b31e79519da6fb67f7d8e8977118832d0f449",
"sha256:d5227b229005a696cc67676e24c214740efd90b148de5733419ac9aaba3773da",
"sha256:e1f57aa70d3f7cc6947fd88636a481638263ba04a742b4a37dd25c373e41491a",
"sha256:e74a55f6bad0e7d3968399deb50f61f4db1926acf4a6d83beaaa7df986f48b1c",
"sha256:e82aba2188b9ba309fd8e271702bd0d0fc9148ae3150532bbb474f4590039ffb",
"sha256:ee69dad2c7155756ad114c02db06002f4cded41132cc51378e57aad79cc8e4f4",
"sha256:f5ab93a2cb2d8338b1674be43b442a7f544a0971da062a5da774ed40587f18f5"
],
"index": "pypi",
"version": "==2.8.6"
},
"pycountry": {
"hashes": [
"sha256:3c57aa40adcf293d59bebaffbe60d8c39976fba78d846a018dc0c2ec9c6cb3cb"
],
"index": "pypi",
"version": "==19.8.18"
},
"python3-openid": {
"hashes": [
"sha256:33fbf6928f401e0b790151ed2b5290b02545e8775f982485205a066f874aaeaf",
"sha256:6626f771e0417486701e0b4daff762e7212e820ca5b29fcc0d05f6f8736dfa6b"
],
"version": "==3.2.0"
},
"pytz": {
"hashes": [
"sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268",
"sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd"
],
"version": "==2020.4"
},
"pyyaml": {
"hashes": [
"sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97",
"sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76",
"sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2",
"sha256:6034f55dab5fea9e53f436aa68fa3ace2634918e8b5994d82f3621c04ff5ed2e",
"sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648",
"sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf",
"sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f",
"sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2",
"sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee",
"sha256:ad9c67312c84def58f3c04504727ca879cb0013b2517c85a9a253f0cb6380c0a",
"sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d",
"sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c",
"sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"
],
"index": "pypi",
"version": "==5.3.1"
},
"redis": {
"hashes": [
"sha256:0e7e0cfca8660dea8b7d5cd8c4f6c5e29e11f31158c0b0ae91a397f00e5a05a2",
"sha256:432b788c4530cfe16d8d943a09d40ca6c16149727e4afe8c2c9d5580c59d9f24"
],
"index": "pypi",
"version": "==3.5.3"
},
"requests": {
"hashes": [
"sha256:7f1a0b932f4a60a1a65caa4263921bb7d9ee911957e0ae4a23a6dd08185ad5f8",
"sha256:e786fa28d8c9154e6a4de5d46a1d921b8749f8b74e28bde23768e5e16eece998"
],
"version": "==2.25.0"
},
"requests-oauthlib": {
"hashes": [
"sha256:7f71572defaecd16372f9006f33c2ec8c077c3cfa6f5911a9a90202beb513f3d",
"sha256:b4261601a71fd721a8bd6d7aa1cc1d6a8a93b4a9f5e96626f8e4d91e8beeaa6a"
],
"version": "==1.3.0"
},
"rules": {
"hashes": [
"sha256:9bae429f9d4f91a375402990da1541f9e093b0ac077221d57124d06eeeca4405"
],
"index": "pypi",
"version": "==2.2"
},
"sqlparse": {
"hashes": [
"sha256:017cde379adbd6a1f15a61873f43e8274179378e95ef3fede90b5aa64d304ed0",
"sha256:0f91fd2e829c44362cbcfab3e9ae12e22badaa8a29ad5ff599f9ec109f0454e8"
],
"version": "==0.4.1"
},
"ua-parser": {
"hashes": [
"sha256:46ab2e383c01dbd2ab284991b87d624a26a08f72da4d7d413f5bfab8b9036f8a",
"sha256:47b1782ed130d890018d983fac37c2a80799d9e0b9c532e734c67cf70f185033"
],
"index": "pypi",
"version": "==0.10.0"
},
"urllib3": {
"hashes": [
"sha256:19188f96923873c92ccb987120ec4acaa12f0461fa9ce5d3d0772bc965a39e08",
"sha256:d8ff90d979214d7b4f8ce956e80f4028fc6860e4431f731ea4a8c08f23f99473"
],
"version": "==1.26.2"
},
"user-agents": {
"hashes": [
"sha256:a98c4dc72ecbc64812c4534108806fb0a0b3a11ec3fd1eafe807cee5b0a942e7",
"sha256:d36d25178db65308d1458c5fa4ab39c9b2619377010130329f3955e7626ead26"
],
"index": "pypi",
"version": "==2.2.0"
},
"vine": {
"hashes": [
"sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87",
"sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af"
],
"version": "==1.3.0"
},
"whitenoise": {
"hashes": [
"sha256:60154b976a13901414a25b0273a841145f77eb34a141f9ae032a0ace3e4d5b27",
"sha256:6dd26bfda3af29177d8ab7333a0c7b7642eb615ce83764f4d15a9aecda3201c4"
],
"index": "pypi",
"version": "==5.1.0"
}
},
"develop": {}
}

View File

@@ -8,7 +8,7 @@
<br>
<strong><a href="#installation">Getting started »</a></strong>
</p>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#features">Features</a> &bull; <a href="https://github.com/milesmcc/a17t">Design</a></p>
<p align="center"><a href="#screenshots">Screenshots</a> &bull; <a href="#features">Features</a> &bull; <a href="https://miles.land/officehours/">Office Hours</a></p>
</p>
<br>

View File

@@ -33,6 +33,8 @@ ACCOUNT_SIGNUPS_ENABLED=False
ACCOUNT_EMAIL_VERIFICATION=none
# The timezone of the admin panel. Affects how dates are displayed.
# This must match a value from the IANA's tz database.
# Wikipedia has a list of valid strings: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
TIME_ZONE=America/New_York
# Set to "False" if you will not be serving content over HTTPS
@@ -72,4 +74,30 @@ SHOW_SHYNET_VERSION=True
# CELERY_BROKER_URL=redis://redis.default.svc.cluster.local/1
# Should Shynet show third-party icons in the dashboard?
SHOW_THIRD_PARTY_ICONS=True
SHOW_THIRD_PARTY_ICONS=True
# Should Shynet block collection of IP addresses globally?
BLOCK_ALL_IPS=False
# Should Shynet include the date and site ID when hashing users?
# This will prevent any possibility of cross-site tracking provided
# that IP collection is also disabled, and external keys (primary
# keys) aren't supplied. It will also prevent sessions from spanning
# one day to another.
AGGRESSIVE_HASH_SALTING=True
# Custom location url to link to in frontend.
# $LATITUDE will get replaced by the latitude, $LONGITUDE will get
# replaced by the longitude.
# Examples:
# - https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE (default)
# - https://www.google.com/maps/search/?api=1&query=$LATITUDE,$LONGITUDE
# - https://www.mapquest.com/near-$LATITUDE,$LONGITUDE
LOCATION_URL=https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE
# How many services should be displayed on dashboard page?
# Set to big number if you don't want pagination at all.
DASHBOARD_PAGE_SIZE=5
# Should background bars be scaled to full width?
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION=True

View File

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

46
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,46 @@
version: '3'
services:
shynet:
container_name: shynet_main
build: .
restart: unless-stopped
expose:
- 8080
env_file:
# Create a file called '.env' if it doesn't already exist.
# You can use `TEMPLATE.env` as a guide.
- .env
environment:
- DB_HOST=db
networks:
- internal
depends_on:
- db
db:
container_name: shynet_database
image: postgres
restart: always
environment:
- "POSTGRES_USER=${DB_USER}"
- "POSTGRES_PASSWORD=${DB_PASSWORD}"
- "POSTGRES_DB=${DB_NAME}"
volumes:
- shynet_db:/var/lib/postgresql/data
networks:
- internal
webserver:
container_name: shynet_webserver
image: nginx
restart: always
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf
ports:
- 8080:80
depends_on:
- shynet
networks:
- internal
volumes:
shynet_db:
networks:
internal:

View File

@@ -61,7 +61,7 @@ spec:
selector:
app: shynet-redis
---
apiVersion: apps/v1beta2
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: shynet-redis
@@ -83,3 +83,37 @@ spec:
ports:
- containerPort: 6379
name: redis
---
apiVersion: v1
kind: Service
metadata:
name: shynet-webserver-service
spec:
type: ClusterIP
ports:
- port: 8080
selector:
app: shynet-webserver
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: shynet-webserver-ingress
annotations:
kubernetes.io/ingress.class: addon-http-application-routing
spec:
rules:
- host: shynet.rmrm.io
http:
paths:
- backend:
serviceName: shynet-webserver-service
servicePort: 8080
path: /
- host: shynet-beta.rmrm.io
http:
paths:
- backend:
serviceName: shynet-webserver-service
servicePort: 8080
path: /

3061
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

1741
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

40
pyproject.toml Normal file
View File

@@ -0,0 +1,40 @@
[tool.poetry]
name = "shynet"
version = "0.10.0"
description = "Modern, privacy-friendly, and cookie-free web analytics."
authors = ["R. Miles McCain <github@sendmiles.email>"]
license = "Apache-2.0"
[tool.poetry.dependencies]
python = "^3.8"
Django = "^3.2.5"
django-allauth = "^0.45.0"
geoip2 = "^4.2.0"
whitenoise = "^5.3.0"
celery = "^5.1.2"
django-ipware = "^3.0.2"
PyYAML = "^5.4.1"
user-agents = "^2.2.0"
rules = "^3.0"
gunicorn = "^20.1.0"
psycopg2-binary = "^2.9.2"
redis = "^3.5.3"
django-redis-cache = "^3.0.0"
pycountry = "^20.7.3"
html2text = "^2020.1.16"
django-health-check = "^3.16.4"
django-npm = "^1.0.0"
python-dotenv = "^0.18.0"
django-debug-toolbar = "^3.2.1"
[tool.poetry.dev-dependencies]
pytest-sugar = "^0.9.4"
factory-boy = "^3.2.0"
pytest-django = "^4.4.0"
django-coverage-plugin = "^2.0.0"
django-stubs = "^1.8.0"
mypy = "^0.910"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@@ -0,0 +1,9 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.litepicker .container__main {
@apply card p-2;
}
}

View File

@@ -28,10 +28,10 @@
{% endfor %}
{% elif field|is_input %}
{% include 'a17t/includes/label.html' %}
{{field|add_class:"input my-1"}}
{{field|add_class:"input ~neutral my-1"}}
{% elif field|is_textarea %}
{% include 'a17t/includes/label.html' %}
{{ field|add_class:'textarea my-1' }}
{{ field|add_class:'textarea ~neutral my-1' }}
{% elif field|is_select %}
{% include 'a17t/includes/label.html' %}
<div class="select {% if field.errors|length > 0 %}~critical{% endif %} my-1">
@@ -39,7 +39,7 @@
</div>
{% else %}
{% include 'a17t/includes/label.html' %}
{{field|add_class:"field my-1"}}
{{field|add_class:"field ~neutral my-1"}}
{% endif %}
{% for error in field.errors %}

View File

@@ -1,6 +1,5 @@
{% load static %}
<link rel="stylesheet" href="{% static 'a17t/dist/a17t.css' %}">
<script async src="{% static '@fortawesome/fontawesome-free/js/all.min.js' %}" data-mutate-approach="sync"></script>
<link href="{% static 'a17t/dist/tailwind.css' %}" rel="stylesheet">
<link href="{% static 'inter-ui/Inter (web)/inter.css' %}" rel="stylesheet">

View File

@@ -1,23 +1,23 @@
<nav class="flex w-full flex-wrap items-center justify-between" role="navigation" aria-label="pagination">
<div class="w-full md:w-auto mb-2">
{% if page.has_previous %}
<a href="?page={{ page.previous_page_number }}{{url_parameters}}" class="button field bg-neutral-000 w-auto mr-1">Previous</a>
<a href="?page={{ page.previous_page_number }}&{{url_parameters}}" class="button ~neutral @low w-auto mr-1">Previous</a>
{% else %}
<a class="button field bg-neutral-000 w-auto mr-1" disabled>Previous</a>
<a class="button ~neutral @low w-auto mr-1" disabled>Previous</a>
{% endif %}
{% if page.has_next %}
<a href="?page={{ page.next_page_number }}{{url_parameters}}" class="button field bg-neutral-000 w-auto">Next</a>
<a href="?page={{ page.next_page_number }}&{{url_parameters}}" class="button ~neutral @low w-auto">Next</a>
{% else %}
<a class="button field bg-neutral-000 w-auto" disabled>Next</a>
<a class="button ~neutral @low w-auto" disabled>Next</a>
{% endif %}
</div>
<ul class="pagination-list w-full md:w-auto mb-2 flex">
{% for pnum in begin %}
{% 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 ~neutral @high w-auto mx-1">{{ pnum }}</a></li>
{% else %}
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
<li><a class="button ~neutral @low w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
{% endfor %}
@@ -25,9 +25,9 @@
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in middle %}
{% 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 ~neutral @high w-auto mx-1">{{ pnum }}</a></li>
{% else %}
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
<li><a class="button ~neutral @low w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
{% endfor %}
{% endif %}
@@ -36,9 +36,9 @@
<li><span class="pagination-ellipsis">&hellip;</span></li>
{% for pnum in end %}
{% 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 ~neutral @high w-auto mx-1">{{ pnum }}</a></li>
{% else %}
<li><a class="button field bg-neutral-000 w-auto mx-1" href="?page={{ pnum }}{{url_parameters}}">{{ pnum }}</a></li>
<li><a class="button ~neutral @low w-auto mx-1" href="?page={{ pnum }}&{{url_parameters}}">{{ pnum }}</a></li>
{% endifequal %}
{% endfor %}
{% endif %}

View File

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

View File

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

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
from django.db import models
from django.shortcuts import reverse
from django.utils import timezone
from core.models import Service
from core.models import Service, ACTIVE_USER_TIMEDELTA
def _default_uuid():
@@ -21,7 +20,7 @@ class Session(models.Model):
# Time
start_time = models.DateTimeField(default=timezone.now, db_index=True)
last_seen = models.DateTimeField(default=timezone.now)
last_seen = models.DateTimeField(default=timezone.now, db_index=True)
# Core request information
user_agent = models.TextField()
@@ -48,16 +47,19 @@ class Session(models.Model):
latitude = models.FloatField(null=True)
time_zone = models.TextField(blank=True)
is_bounce = models.BooleanField(default=True, db_index=True)
class Meta:
ordering = ["-start_time"]
indexes = [
models.Index(fields=["service", "-start_time"]),
models.Index(fields=["service", "-last_seen"]),
models.Index(fields=["service", "identifier"]),
]
@property
def is_currently_active(self):
return timezone.now() - self.last_seen < timezone.timedelta(seconds=10)
return timezone.now() - self.last_seen < ACTIVE_USER_TIMEDELTA
@property
def duration(self):
@@ -72,6 +74,12 @@ class Session(models.Model):
kwargs={"pk": self.service.pk, "session_pk": self.uuid},
)
def recalculate_bounce(self):
bounce = self.hit_set.count() == 1
if bounce != self.is_bounce:
self.is_bounce = bounce
self.save()
class Hit(models.Model):
session = models.ForeignKey(Session, on_delete=models.CASCADE, db_index=True)
@@ -79,7 +87,7 @@ class Hit(models.Model):
# Base request information
start_time = models.DateTimeField(default=timezone.now, db_index=True)
last_seen = models.DateTimeField(default=timezone.now)
last_seen = models.DateTimeField(default=timezone.now, db_index=True)
heartbeats = models.IntegerField(default=0)
tracker = models.TextField(
choices=[("JS", "JavaScript"), ("PIXEL", "Pixel (noscript)")]
@@ -88,12 +96,17 @@ class Hit(models.Model):
# Advanced page information
location = models.TextField(blank=True, db_index=True)
referrer = models.TextField(blank=True, db_index=True)
load_time = models.FloatField(null=True)
load_time = models.FloatField(null=True, db_index=True)
# While not necessary, we store the root service directly for performance.
# It makes querying much easier; no need for inner joins.
service = models.ForeignKey(Service, on_delete=models.CASCADE, db_index=True)
class Meta:
ordering = ["-start_time"]
indexes = [
models.Index(fields=["session", "-start_time"]),
models.Index(fields=["service", "-start_time"]),
models.Index(fields=["session", "location"]),
models.Index(fields=["session", "referrer"]),
]
@@ -105,5 +118,5 @@ class Hit(models.Model):
def get_absolute_url(self):
return reverse(
"dashboard:service_session",
kwargs={"pk": self.session.service.pk, "session_pk": self.session.pk},
kwargs={"pk": self.service.pk, "session_pk": self.session.pk},
)

View File

@@ -1,5 +1,4 @@
import ipaddress
import json
import logging
from hashlib import sha256
@@ -9,6 +8,7 @@ from celery import shared_task
from django.conf import settings
from django.core.cache import cache
from django.db.models import Q
from django.utils import timezone
from core.models import Service
@@ -39,6 +39,9 @@ def _geoip2_lookup(ip):
}
except geoip2.errors.AddressNotFoundError:
return {}
except FileNotFoundError as e:
log.exception("Unable to perform GeoIP lookup: %s", e)
return {}
@shared_task
@@ -58,6 +61,7 @@ def ingress_request(
log.debug(f"Linked to service {service}")
if dnt and service.respect_dnt:
log.debug("Ignoring because of DNT")
return
try:
@@ -67,6 +71,7 @@ def ingress_request(
ignored_network.version == remote_ip.version
and ignored_network.supernet_of(remote_ip)
):
log.debug("Ignoring because of ignored IP")
return
except ValueError as e:
log.exception(e)
@@ -78,6 +83,11 @@ def ingress_request(
association_id_hash = sha256()
association_id_hash.update(str(ip).encode("utf-8"))
association_id_hash.update(str(user_agent).encode("utf-8"))
if settings.AGGRESSIVE_HASH_SALTING:
association_id_hash.update(str(service.pk).encode("utf-8"))
association_id_hash.update(
str(timezone.now().date().isoformat()).encode("utf-8")
)
session_cache_path = (
f"session_association_{service.pk}_{association_id_hash.hexdigest()}"
)
@@ -116,7 +126,7 @@ def ingress_request(
return
session = Session.objects.create(
service=service,
ip=ip if service.collect_ips else None,
ip=ip if service.collect_ips and not settings.BLOCK_ALL_IPS else None,
user_agent=user_agent,
identifier=identifier.strip(),
browser=ua.browser.family or "",
@@ -179,7 +189,12 @@ def ingress_request(
load_time=payload.get("loadTime"),
start_time=time,
last_seen=time,
service=service,
)
# Recalculate whether the session is a bounce
session.recalculate_bounce()
# Set idempotency (if applicable)
if idempotency is not None:
cache.set(
@@ -187,4 +202,5 @@ def ingress_request(
)
except Exception as e:
log.exception(e)
print(e)
raise e

View File

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

View File

@@ -54,7 +54,7 @@ class ValidateServiceOriginsMixin:
origins = service.origins
cache.set(f"service_origins_{service_uuid}", origins, timeout=3600)
resp = super().dispatch(request, *args, **kwargs)
allow_origin = "*"
if origins != "*":
remote_origin = request.META.get("HTTP_ORIGIN")
@@ -66,12 +66,12 @@ class ValidateServiceOriginsMixin:
remote_origin = f"{parsed.scheme}://{parsed.netloc}".lower()
origins = [origin.strip().lower() for origin in origins.split(",")]
if remote_origin in origins:
resp["Access-Control-Allow-Origin"] = remote_origin
allow_origin = remote_origin
else:
return HttpResponseForbidden()
else:
resp["Access-Control-Allow-Origin"] = "*"
resp = super().dispatch(request, *args, **kwargs)
resp["Access-Control-Allow-Origin"] = allow_origin
resp["Access-Control-Allow-Methods"] = "GET,HEAD,OPTIONS,POST"
resp[
"Access-Control-Allow-Headers"
@@ -112,7 +112,9 @@ class ScriptView(ValidateServiceOriginsMixin, View):
endpoint = (
reverse(
"ingress:endpoint_script",
kwargs={"service_uuid": self.kwargs.get("service_uuid"),},
kwargs={
"service_uuid": self.kwargs.get("service_uuid"),
},
)
if self.kwargs.get("identifier") == None
else reverse(

37
shynet/core/factories.py Normal file
View File

@@ -0,0 +1,37 @@
from django.contrib.auth import get_user_model
import factory
from factory.django import DjangoModelFactory
from .models import Service
class UserFactory(DjangoModelFactory):
username = factory.Faker("user_name")
email = factory.Faker("email")
name = factory.Faker("name")
@post_generation
def password(self, create, extracted, **kwargs):
password = (
extracted
if extracted
else factory.Faker(
"password",
length=42,
special_chars=True,
digits=True,
upper_case=True,
lower_case=True,
).evaluate(None, None, extra={"locale": None})
)
self.set_password(password)
class Meta:
model = get_user_model()
django_get_or_create = ["username"]
class ServiceFactory(DjangoModelFactory):
class Meta:
model = Service
name = factory.Faker("company")

View File

@@ -54,15 +54,18 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"name", type=str,
"name",
type=str,
)
parser.add_argument("owner_email", type=str)
parser.add_argument(
"avg", type=int,
"avg",
type=int,
)
parser.add_argument("deviation", type=float, default=0.4)
parser.add_argument(
"days", type=int,
"days",
type=int,
)
parser.add_argument("load_time", type=float, default=1000)

View File

@@ -1,34 +0,0 @@
import traceback
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
from core.models import User
class Command(BaseCommand):
help = "Configures the Shynet hostname"
def add_arguments(self, parser):
parser.add_argument(
"hostname", type=str,
)
def handle(self, *args, **options):
site = Site.objects.get(pk=settings.SITE_ID)
site.domain = options.get("hostname")
if options.get("hostname").lower().startswith("http"):
self.stdout.write(
self.style.WARNING(
f"Warning: the hostname '{options.get('hostname')}' starts with `http`. You almost certainly don't want this. The hostname is supposed to be the raw domain name of your Shynet instance, without `http://` or `https://`. For example, if your Shynet instance will eventually be hosted at `https://analytics.example.com`, the hostname should be `analytics.example.com`."
)
)
site.save()
self.stdout.write(
self.style.SUCCESS(
f"Successfully set the hostname to '{options.get('hostname')}'"
)
)

View File

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

View File

@@ -35,15 +35,9 @@ class Command(BaseCommand):
def handle(self, *args, **options):
migration = self.check_migrations()
admin, hostname, whitelabel = [True] * 3
admin, whitelabel = [True] * 2
if not migration:
admin = not User.objects.all().exists()
hostname = (
not Site.objects.filter(domain__isnull=False)
.exclude(domain__exact="")
.exclude(domain__exact="example.com")
.exists()
)
whitelabel = (
not Site.objects.filter(name__isnull=False)
.exclude(name__exact="")
@@ -51,6 +45,4 @@ class Command(BaseCommand):
.exists()
)
self.stdout.write(
self.style.SUCCESS(f"{migration} {admin} {hostname} {whitelabel}")
)
self.stdout.write(self.style.SUCCESS(f"{migration} {admin} {whitelabel}"))

View File

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

View File

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

View File

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

View File

@@ -4,14 +4,20 @@ import re
import uuid
from django.apps import apps
from django.conf import settings
from django.contrib.auth.models import AbstractUser
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models.functions import TruncDate
from django.db.models.functions import TruncDate, TruncHour
from django.db.utils import NotSupportedError
from django.shortcuts import reverse
from django.utils import timezone
# How long a session a needs to go without an update to no longer be considered 'active' (i.e., currently online)
ACTIVE_USER_TIMEDELTA = timezone.timedelta(
milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2
)
def _default_uuid():
return str(uuid.uuid4())
@@ -119,8 +125,10 @@ class Service(models.Model):
Session = apps.get_model("analytics", "Session")
Hit = apps.get_model("analytics", "Hit")
tz_now = timezone.now()
currently_online = Session.objects.filter(
service=self, last_seen__gt=timezone.now() - timezone.timedelta(seconds=10)
service=self, last_seen__gt=tz_now - ACTIVE_USER_TIMEDELTA
).count()
sessions = Session.objects.filter(
@@ -129,11 +137,13 @@ class Service(models.Model):
session_count = sessions.count()
hits = Hit.objects.filter(
session__service=self, start_time__lt=end_time, start_time__gt=start_time
service=self, start_time__lt=end_time, start_time__gt=start_time
)
hit_count = hits.count()
bounces = sessions.annotate(hit_count=models.Count("hit")).filter(hit_count=1)
has_hits = Hit.objects.filter(service=self).exists()
bounces = sessions.filter(is_bounce=True)
bounce_count = bounces.count()
locations = (
@@ -188,6 +198,36 @@ class Service(models.Model):
avg_hits_per_session = hit_count / session_count if session_count > 0 else None
avg_session_duration = self._get_avg_session_duration(sessions, session_count)
chart_data, chart_tooltip_format, chart_granularity = self._get_chart_data(
sessions, hits, start_time, end_time, tz_now
)
return {
"currently_online": currently_online,
"session_count": session_count,
"hit_count": hit_count,
"has_hits": has_hits,
"bounce_rate_pct": bounce_count * 100 / session_count
if session_count > 0
else None,
"avg_session_duration": avg_session_duration,
"avg_load_time": avg_load_time,
"avg_hits_per_session": avg_hits_per_session,
"locations": locations,
"referrers": referrers,
"countries": countries,
"operating_systems": operating_systems,
"browsers": browsers,
"devices": devices,
"device_types": device_types,
"chart_data": chart_data,
"chart_tooltip_format": chart_tooltip_format,
"chart_granularity": chart_granularity,
"online": True,
}
def _get_avg_session_duration(self, sessions, session_count):
try:
avg_session_duration = sessions.annotate(
duration=models.F("last_seen") - models.F("start_time")
@@ -202,46 +242,77 @@ class Service(models.Model):
if session_count == 0:
avg_session_duration = None
session_chart_data = {
k["date"]: k["count"]
for k in sessions.annotate(date=TruncDate("start_time"))
.values("date")
.annotate(count=models.Count("uuid"))
.order_by("date")
}
for day_offset in range((end_time - start_time).days + 1):
day = (start_time + timezone.timedelta(days=day_offset)).date()
if day not in session_chart_data:
session_chart_data[day] = 0
return avg_session_duration
return {
"currently_online": currently_online,
"session_count": session_count,
"hit_count": hit_count,
"avg_hits_per_session": hit_count / (max(session_count, 1)),
"bounce_rate_pct": bounce_count * 100 / session_count
if session_count > 0
else None,
"avg_session_duration": avg_session_duration,
"avg_load_time": avg_load_time,
"avg_hits_per_session": avg_hits_per_session,
"locations": locations,
"referrers": referrers,
"countries": countries,
"operating_systems": operating_systems,
"browsers": browsers,
"devices": devices,
"device_types": device_types,
"session_chart_data": json.dumps(
[
{"x": str(key), "y": value}
for key, value in sorted(
session_chart_data.items(), key=lambda k: k[0]
)
]
),
"online": True,
def _get_chart_data(self, sessions, hits, start_time, end_time, tz_now):
# Show hourly chart for date ranges of 3 days or less, otherwise daily chart
if (end_time - start_time).days < 3:
chart_tooltip_format = "MM/dd HH:mm"
chart_granularity = "hourly"
sessions_per_hour = (
sessions.annotate(hour=TruncHour("start_time"))
.values("hour")
.annotate(count=models.Count("uuid"))
.order_by("hour")
)
chart_data = {
k["hour"]: {"sessions": k["count"]} for k in sessions_per_hour
}
hits_per_hour = (
hits.annotate(hour=TruncHour("start_time"))
.values("hour")
.annotate(count=models.Count("id"))
.order_by("hour")
)
for k in hits_per_hour:
if k["hour"] not in chart_data:
chart_data[k["hour"]] = {"hits": k["count"], "sessions": 0}
else:
chart_data[k["hour"]]["hits"] = k["count"]
hours_range = range(int((end_time - start_time).total_seconds() / 3600) + 1)
for hour_offset in hours_range:
hour = start_time + timezone.timedelta(hours=hour_offset)
if hour not in chart_data and hour <= tz_now:
chart_data[hour] = {"sessions": 0, "hits": 0}
else:
chart_tooltip_format = "MMM d"
chart_granularity = "daily"
sessions_per_day = (
sessions.annotate(date=TruncDate("start_time"))
.values("date")
.annotate(count=models.Count("uuid"))
.order_by("date")
)
chart_data = {k["date"]: {"sessions": k["count"]} for k in sessions_per_day}
hits_per_day = (
hits.annotate(date=TruncDate("start_time"))
.values("date")
.annotate(count=models.Count("id"))
.order_by("date")
)
for k in hits_per_day:
if k["date"] not in chart_data:
chart_data[k["date"]] = {"hits": k["count"], "sessions": 0}
else:
chart_data[k["date"]]["hits"] = k["count"]
for day_offset in range((end_time - start_time).days + 1):
day = (start_time + timezone.timedelta(days=day_offset)).date()
if day not in chart_data and day <= tz_now.date():
chart_data[day] = {"sessions": 0, "hits": 0}
chart_data = sorted(chart_data.items(), key=lambda k: k[0])
chart_data = {
"sessions": [v["sessions"] for k, v in chart_data],
"hits": [v["hits"] for k, v in chart_data],
"labels": [str(k) for k, v in chart_data],
}
return chart_data, chart_tooltip_format, chart_granularity
def get_absolute_url(self):
return reverse("dashboard:service", kwargs={"pk": self.pk},)
return reverse(
"dashboard:service",
kwargs={"pk": self.pk},
)

View File

@@ -0,0 +1 @@

View File

@@ -1,5 +1,6 @@
from allauth.account.admin import EmailAddress
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _
from core.models import Service, User
@@ -33,7 +34,6 @@ class ServiceForm(forms.ModelForm):
labels = {
"origins": "Allowed origins",
"respect_dnt": "Respect DNT",
"collect_ips": "Collect IP addresses",
"ignored_ips": "Ignored IP addresses",
"ignore_robots": "Ignore robots",
"hide_referrer_regex": "Hide specific referrers",
@@ -46,13 +46,27 @@ class ServiceForm(forms.ModelForm):
"At what origins does the service operate? Use commas to separate multiple values. This sets CORS headers, so use '*' if you're not sure (or don't care)."
),
"respect_dnt": "Should visitors who have enabled <a href='https://en.wikipedia.org/wiki/Do_Not_Track'>Do Not Track</a> be excluded from all data?",
"collect_ips": "Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected.",
"ignored_ips": "A comma-separated list of IP addresses or IP ranges (IPv4 and IPv6) to exclude from tracking (e.g., '192.168.0.2, 127.0.0.1/32').",
"ignore_robots": "Should sessions generated by bots be excluded from tracking?",
"hide_referrer_regex": "Any referrers that match this <a href='https://regexr.com/'>RegEx</a> will not be listed in the referrer summary. Sessions will still be tracked normally. No effect if left blank.",
"script_inject": "Optional additional JavaScript to inject at the end of the Shynet script. This code will be injected on every page where this service is installed.",
}
collect_ips = forms.BooleanField(
help_text="IP address collection is disabled globally by your administrator."
if settings.BLOCK_ALL_IPS
else "Should individual IP addresses be collected? IP metadata (location, host, etc) will still be collected.",
widget=forms.RadioSelect(choices=[(True, "Yes"), (False, "No")]),
initial=False if settings.BLOCK_ALL_IPS else True,
required=False,
disabled=settings.BLOCK_ALL_IPS,
)
def clean_collect_ips(self):
collect_ips = self.cleaned_data["collect_ips"]
# Forces collect IPs to be false if it is disabled globally
return False if settings.BLOCK_ALL_IPS else collect_ips
collaborators = forms.CharField(
help_text="Which users on this Shynet instance should have read-only access to this service? (Comma separated list of emails.)",
required=False,

View File

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

View File

@@ -15,16 +15,26 @@
text-align: right !important;
}
: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;
.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;
}
.min-w-48 {
min-width: 48px;
}
.geo-table {
display: none;
}
.geo-card--use-table-view .geo-map {
display: none;
}
.geo-card--use-table-view .geo-table {
display: inline-block;
}

View File

@@ -1,12 +1,12 @@
{% extends "base.html" %}
{% block content %}
<div>
<h4 class="heading">{% block page_title %}{% endblock %}</h4>
<div class="flex-1 truncate">
<h4 class="heading truncate">{% block page_title %}{% endblock %}</h4>
</div>
<hr class="sep">
{% block main %}
<div class="card ~neutral !low max-w-lg content">
<div class="card max-w-lg content">
{% block card %}
{% endblock %}
</div>

View File

@@ -6,7 +6,7 @@
{% block page_title %}{% trans "Email Addresses" %}{% endblock %}
{% block main %}
<div class="card ~neutral !low max-w-lg">
<div class="card max-w-lg">
{% if user.emailaddress_set.all %}
<p>{% trans 'These are your known email addresses:' %}</p>
@@ -33,7 +33,7 @@
{% endfor %}
<div class="block mt-4">
<button class="button ~neutral !high mb-1" type="submit" name="action_primary">{% trans 'Make Primary' %}</button>
<button class="button ~neutral @high mb-1" type="submit" name="action_primary">{% trans 'Make Primary' %}</button>
<button class="button ~neutral mb-1" type="submit" name="action_send">{% trans 'Resend Verification' %}</button>
<button class="button ~neutral mb-1" type="submit" name="action_remove">{% trans 'Remove' %}</button>
</div>
@@ -51,10 +51,10 @@
<hr class="sep">
<form method="post" action="{% url 'account_email' %}" class="card ~neutral !low max-w-lg">
<form method="post" action="{% url 'account_email' %}" class="card max-w-lg">
{% csrf_token %}
{{ form|a17t }}
<button name="action_add" class="button ~neutral !high" type="submit">{% trans "Add Address" %}</button>
<button name="action_add" class="button ~neutral @high" type="submit">{% trans "Add Address" %}</button>
</form>
{% endblock %}

View File

@@ -1,10 +1,10 @@
{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hi there,
{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=request.get_host %}Hi there,
You're receiving this email because {{ user_display }} has listed this email as a valid contact address for their account.
To confirm this is correct, go to {{ activate_url }}
{% endblocktrans %}
{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you,
{% blocktrans with site_name=current_site.name site_domain=request.get_host %}Thank you,
{{ site_name }}
{% endblocktrans %}
{% endautoescape %}

View File

@@ -1,4 +1,4 @@
{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Hi there,
{% load i18n %}{% autoescape off %}{% blocktrans with site_name=current_site.name site_domain=request.get_host %}Hi there,
You're receiving this email because you or someone else has requested a password for your account.
@@ -6,7 +6,7 @@ This message can be safely ignored if you did not request a password reset. Clic
{{ password_reset_url }}
{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}Thank you,
{% blocktrans with site_name=current_site.name site_domain=request.get_host %}Thank you,
{{ site_name }}
{% endblocktrans %}
{% endautoescape %}

View File

@@ -18,7 +18,7 @@
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
{% csrf_token %}
<button type="submit" class="button ~positive !high">{% trans 'Confirm' %}</button>
<button type="submit" class="button ~positive @high">{% trans 'Confirm' %}</button>
</form>
{% else %}

View File

@@ -16,7 +16,7 @@
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<button class="button ~urge !high mr-2" type="submit">{% trans "Sign In" %}</button>
<button class="button ~urge @high mr-2" type="submit">{% trans "Sign In" %}</button>
<a href="{% url 'account_reset_password' %}" class="button ~neutral mr-2">{% trans "Reset Password" %}</a>
<a href="{{ signup_url }}" class="button ~neutral">Sign Up</a>
</form>

View File

@@ -13,6 +13,6 @@
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
{% endif %}
<button type="submit" class="button ~neutral !high">{% trans 'Sign Out' %}</button>
<button type="submit" class="button ~neutral @high">{% trans 'Sign Out' %}</button>
</form>
{% endblock %}

View File

@@ -9,6 +9,6 @@
<form method="POST" action="{% url 'account_change_password' %}" class="password_change max-w-lg">
{% csrf_token %}
{{ form|a17t }}
<button type="submit" name="action" class="button ~urge !high">{% trans "Change Password" %}</button>
<button type="submit" name="action" class="button ~urge @high">{% trans "Change Password" %}</button>
</form>
{% endblock %}

View File

@@ -18,6 +18,6 @@
<form method="POST" action="{% url 'account_reset_password' %}" class="password_reset max-w-lg">
{% csrf_token %}
{{ form|a17t }}
<button type="submit" class="button ~urge !high">{% trans 'Reset Password' %}</button>
<button type="submit" class="button ~urge @high">{% trans 'Reset Password' %}</button>
</form>
{% endblock %}

View File

@@ -13,7 +13,7 @@
<form method="POST" action="{{ action_url }}" class="max-w-lg">
{% csrf_token %}
{{ form|a17t }}
<button type="submit" name="action" class="button ~urge !high">{% trans 'Change Password' %}</button>
<button type="submit" name="action" class="button ~urge @high">{% trans 'Change Password' %}</button>
</form>
{% else %}
<p>{% trans 'Your password is now changed.' %}</p>

View File

@@ -6,5 +6,5 @@
{% block card %}
<p>{% trans 'Your password is now changed.' %}</p>
<a href="{% url 'account_login' %}" class="button ~urge !high">Log In</a>
<a href="{% url 'account_login' %}" class="button ~urge @high">Log In</a>
{% endblock %}

View File

@@ -9,6 +9,6 @@
<form method="POST" action="{% url 'account_set_password' %}" class="password_set">
{% csrf_token %}
{{ form|a17t }}
<button type="submit" name="action" class="button ~urge !high">{% trans 'Set Password' %}</button>
<button type="submit" name="action" class="button ~urge @high">{% trans 'Set Password' %}</button>
</form>
{% endblock %}

View File

@@ -14,7 +14,7 @@
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<button type="submit" class="button ~urge !high">{% trans "Sign Up" %}</button>
<button type="submit" class="button ~urge @high">{% trans "Sign Up" %}</button>
</form>
{% endblock %}

View File

@@ -10,31 +10,36 @@
{% include 'a17t/includes/head.html' %}
<link rel="icon" type="image/png" href="{% static 'dashboard/images/icon.png' %}">
<script src="{% static 'apexcharts/dist/apexcharts.min.js'%}"></script>
<script src="{% static 'litepicker/dist/js/main.js' %}"></script>
<script src="{% static 'turbolinks/dist/turbolinks.js' %}"></script>
<script src="{% static 'litepicker/dist/nocss/litepicker.js' %}"></script>
<script src="{% static 'litepicker/dist/plugins/ranges.js' %}"></script>
<script src="{% static 'd3/d3.min.js' %}"></script>
<script src="{% static 'topojson/build/topojson.min.js' %}"></script>
<script src="{% static 'datamaps/dist/datamaps.world.min.js' %}"></script>
<script src="{% static 'dashboard/js/base.js' %}"></script>
<link rel="stylesheet" href="{% static 'dashboard/css/global.css' %}">
<link rel="stylesheet" href="{% static 'flag-icon-css/css/flag-icon.min.css' %}">
<link rel="stylesheet" href="{% static 'litepicker/dist/css/litepicker.css' %}">
{% block extra_head %}
{% endblock %}
</head>
<body class="bg-neutral-100 min-h-full">
<body class="bg-neutral-50 min-h-full overflow-x-hidden">
{% block body %}
<section class="max-w-screen-xl mx-auto px-4 py-4 md:py-12 md:flex">
<aside
class="mb-8 md:w-2/12 md:pr-6 relative flex flex-wrap md:block justify-between items-center overflow-x-hidden">
class="mb-2 md:w-2/12 md:pr-6 relative flex flex-wrap md:block justify-between items-center overflow-x-hidden">
<a class="icon ~urge ml-2 md:ml-6 md:mb-8 md:mt-3" href="{% url 'dashboard:dashboard' %}">
<i class="fas fa-binoculars fa-3x text-urge-600 hidden md:block"></i>
<i class="fas fa-binoculars fa-2x text-urge-600 md:hidden"></i>
</a>
<button class="button ~neutral !low md:hidden"
<a tabindex="0" role="button" class="text-neutral-600 md:hidden"
onclick="document.getElementById('navMenuExpanded').classList.toggle('hidden')">
<span class="icon">
<i class="fas fa-bars"></i>
</span>
</button>
</a>
<hr class="sep h-4 md:h-8 w-full">
<div id="navMenuExpanded"
class="bg-neutral-000 shadow-lg md:shadow-none p-4 hidden rounded-lg md:block md:bg-transparent md:border-none md:p-0 w-full">
@@ -42,8 +47,8 @@
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Services</p>
{% for service in user.owning_services.all %}
{% url 'dashboard:service' service.uuid as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name|truncatechars:16 url=url icon=service.link|iconify %}
{% contextual_url 'dashboard:service' service.uuid as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name url=url icon=service.link|iconify %}
{% endfor %}
{% endif %}
@@ -61,8 +66,8 @@
<p class="ml-2 mb-1 supra font-medium text-gray-500 pointer-events-none">Collaborations</p>
{% for service in user.collaborating_services.all %}
{% url 'dashboard:service' service.uuid as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name|truncatechars:20 url=url %}
{% contextual_url 'dashboard:service' service.uuid as url %}
{% include 'dashboard/includes/sidebar_portal.html' with label=service.name url=url %}
{% endfor %}
<hr class="sep h-8">
@@ -105,7 +110,7 @@
{% if messages %}
<div>
{% for message in messages %}
<article class="card {{message.tags}} !high mb-2 w-full">{{message}}</article>
<article class="card {{message.tags}} @high mb-2 w-full">{{message}}</article>
{% endfor %}
</ul>
</div>
@@ -122,4 +127,4 @@
{% endblock %}
</body>
</html>
</html>

View File

@@ -0,0 +1,6 @@
{% load helpers %}
<div
class="absolute h-6 rounded-md"
style="width: {% bar_width count max total %}; top: 6px; left: 0px; height: calc(100% - 12px); background-color: var(--color-urge-100-fallback)"
>
</div>

View File

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

View File

@@ -0,0 +1,66 @@
{% load helpers %}
<div id="map-chart" class="relative"></div>
<script>
// Colors
const lightBlue = "#C4B5FD";
const highlightBlue = "#8B5CF6";
const white = "#ffffff";
// Data maps
const countryMapData = {};
const countryMapColors = {};
const countryMap = {
{% for country in countries %}"{{country.country|safe|datamap_id}}": {{country.count}},
{% endfor %}
};
// Max session count will be full opacity
const maxSessionCount = Math.max(...Object.values(countryMap));
// Color scale starts from opacity 0.1 - 1.0, 0 sessions gets opacity 0
const minPercentage = 0.1
// Loop over country map and transform data for Datamaps use
const keys = Object.keys(countryMap);
const length = keys.length;
for (let i = 0; i < length; i++) {
countryMapData[keys[i]] = {
sessionCount: countryMap[keys[i]],
color: `rgba(124, 58, 237, ${countryMap[keys[i]] === 0 ? 0 : minPercentage + (countryMap[keys[i]] / maxSessionCount * (1 - minPercentage))})`
};
countryMapColors[keys[i]] = countryMapData[keys[i]].color;
}
// Create datamap
var geoMap = new Datamap({
element: document.getElementById('map-chart'),
projection: 'mercator',
responsive: true,
geographyConfig: {
borderColor: lightBlue,
highlightBorderColor: highlightBlue,
highlightBorderWidth: 1.5,
highlightFillColor: (geography) => geography.color || white,
highlightFillOpacity: 0.9,
popupTemplate: (geography, data) => '<div class="hoverinfo"><strong>' + geography.properties.name + '</strong>: ' + data.sessionCount + ' sessions</div>'
},
fills: {
defaultFill: white
},
data: countryMapData,
aspectRatio: 0.68
});
geoMap.updateChoropleth(countryMapColors);
let debounceTimeout
const debounce = (func, debounce) => {
return function(event){
if(debounceTimeout) clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(func,debounce,event);
};
}
window.addEventListener("resize",debounce(() => geoMap.resize(), 100))
</script>

View File

@@ -1,16 +1,16 @@
{% load humanize helpers %}
<a class="card ~neutral !low service mb-6 p-0" href="{% url 'dashboard:service' object.uuid %}">
<a class="card chart-card overflow-visible service mb-6 p-0" href="{% contextual_url 'dashboard:service' object.uuid %}">
{% with stats=object.stats %}
<div class="p-4 md:flex justify-between">
<div class="flex items-center mb-4 md:mb-0">
<h3 class="heading text-xl md:text-2xl mr-2 mb-1 text-urge-600 flex items-center">
<div class="p-4 md:flex justify-between overflow-none">
<div class="flex items-center mb-4 md:mb-0 md:flex-1 md:min-w-0 truncate pr-0 md:pr-2">
<h3 class="heading text-xl md:text-2xl mr-2 mb-1 text-urge-600 flex items-center truncate" title="{{object.name}}">
{{object.link|iconify}}
<span>{{object.name}}</span>
<span class="truncate">{{object.name}}</span>
</h3>
{% include 'dashboard/includes/stats_status_chip.html' %}
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
<div class="grid grid-cols-2 md:grid-cols-4 gap-6 md:gap-3 lg:gap-6 md:flex-none">
<div>
<p>Sessions</p>
<p class="label">
@@ -51,7 +51,7 @@
</div>
<hr class="sep h-4">
<div style="bottom: -1px;">
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data sparkline=True height=100 name=object.uuid %}
{% include 'dashboard/includes/time_chart.html' with data=stats.chart_data sparkline=True height=100 name=object.uuid tooltip_format=stats.chart_tooltip_format granularity=stats.chart_granularity %}
</div>
{% 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.get_host}}{% url 'ingress:endpoint_pixel' object.uuid %}">
</noscript>
<script defer src="{{script_protocol}}{{request.get_host}}{% url 'ingress:endpoint_script' object.uuid %}"></script>{% endfilter %}
</div>

View File

@@ -12,7 +12,7 @@
{% for session in object_list %}
<tr>
<td>
<a href="{% url 'dashboard:service_session' object.pk session.pk %}"
<a href="{% contextual_url 'dashboard:service_session' object.pk session.pk %}"
class="font-medium text-urge-700">
{{session.start_time|date:"M j Y, g:i a"|capfirst}}
{% if session.is_currently_active %}
@@ -27,7 +27,7 @@
<span class="text-gray-600">&mdash;</span>
{% endif %}
</td>
<td>{{session.country|flag_emoji}} {{session.asn|default:"Unknown"}}</td>
<td><span class="{{session.country|flag_class}}"></span>{{session.asn|default:"Unknown"}}</td>
<td class="rf">{{session.duration|naturaldelta}}</td>
<td class="rf">{{session.hit_set.count|intcomma}}</td>
</tr>

View File

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

View File

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

View File

@@ -5,9 +5,14 @@
enabled: false
},
tooltip: {
shared: false,
shared: true,
x: {
format: '{{tooltip_format|default:"MMM d"}}',
},
},
legend: {
show: false,
},
colors: ["#805AD5"],
chart: {
zoom: {
enabled: false,
@@ -15,7 +20,7 @@
toolbar: {
show: false,
},
type: 'area',
type: 'line',
height: {{height|default:"200"}},
offsetY: -1,
animations: {
@@ -24,16 +29,14 @@
sparkline: {
enabled: {% if sparkline %}true{% else %}false{% endif %},
},
fill: {
type: 'gradient',
gradient: {
shadeIntensity: 1,
inverseColors: false,
opacityFrom: 0.8,
opacityTo: 0,
stops: [0, 75, 100]
{% if granularity == "daily" and click_zoom %}
events: {
markerClick: function(event, chartContext, { seriesIndex, dataPointIndex, w: {config}}) {
const day = config.labels[dataPointIndex]
window.location.href = `?startDate=${day}&endDate=${day}`
},
},
{% endif %}
},
grid: {
padding: {
@@ -63,15 +66,27 @@
},
xaxis: {
type: "datetime",
labels: {
datetimeUTC: false
},
},
stroke: {
width: 1.5,
width: 2,
curve: 'smooth',
},
series: [{
name: "{{unit|default:'Sessions'}}",
data: {{data|safe}}
}]
name: "Hits",
type: 'area',
color: "#ddd6fe",
data: {{data.hits|safe}}
}, {
name: "Sessions",
type: 'line',
color: "#805AD5",
data: {{data.sessions|safe}}
}],
labels: {{data.labels|safe}}
};
var triggerMatchesChart = new ApexCharts(document.querySelector("#chart{{name|default:'Main'}}"), triggerMatchesChartOptions);
triggerMatchesChart.render();
</script>
</script>

View File

@@ -1,11 +1,11 @@
{% extends "base.html" %}
{% load rules %}
{% load rules pagination %}
{% block content %}
<div class="md:flex justify-between items-center">
<div>
<h4 class="heading">{{request.site.name|default:"Dashboard"}}</h4>
<div class="flex-1 truncate display-none md:display-block mr-4 md:mb-0 mb-4" title="{{request.site.name|default:"Dashboard"}}">
<h4 class="heading truncate">{{request.site.name|default:"Dashboard"}}</h4>
</div>
<div class="flex items-center">
<div class="mr-1">
@@ -13,14 +13,19 @@
</div>
{% has_perm "core.create_service" user as can_create %}
{% if can_create %}
<a href="{% url 'dashboard:service_create' %}" class="button field bg-neutral-000 w-auto">+ New Service</a>
<a href="{% url 'dashboard:service_create' %}" class="button ~neutral @low w-auto">+ New Service</a>
{% endif %}
</div>
</div>
<hr class="sep">
{% for object in services|dictsortreversed:"stats.session_count" %}
<hr class="sep h-8 md:h-12">
{% for object in object_list|dictsortreversed:"stats.session_count" %}
{% include 'dashboard/includes/service_overview.html' %}
{% empty %}
<p>You don't have any services on {{request.site.name|default:"Shynet"}} yet.</p>
<p class="aside ~urge @high">You don't have any services yet. {% if can_create %}Get started by <a href="{% url 'dashboard:service_create' %}" class="underline">creating one</a>.{% endif %}</p>
{% endfor %}
{% if object_list %}
{% pagination page_obj request %}
{% endif %}
{% endblock %}

View File

@@ -4,6 +4,6 @@
<section class="content">
<h2>{{request.site.name}} Analytics</h2>
<p>{{request.site.name}} uses Shynet. Eventually, more information about Shynet will be available here.</p>
<a href="{% url 'account_login' %}" class="button ~urge !high">Log In</a>
<a href="{% url 'account_login' %}" class="button ~urge @high">Log In</a>
</section>
{% endblock %}

View File

@@ -6,12 +6,20 @@
<div class="mr-2">{% include 'dashboard/includes/date_range.html' %}</div>
{% has_perm 'core.change_service' user object as can_update %}
{% if can_update %}
<a href="{% url 'dashboard:service_update' service.uuid %}" class="button field bg-neutral-000 w-auto">Manage &rarr;</a>
<a href="{% contextual_url 'dashboard:service_update' service.uuid %}" class="button ~neutral @low w-auto">Manage &rarr;</a>
{% endif %}
{% endblock %}
{% block service_content %}
<div class="grid grid-cols-2 gap-6 md:flex justify-between mb-6 card ~neutral !high px-6" id="stats">
{% 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">
{% with classes="text-sm font-semibold" good_classes="text-positive-400" bad_classes="text-critical-400" neutral_classes="text-gray-400" %}
<article class="">
<p class="label text-gray-400">Sessions</p>
@@ -85,11 +93,12 @@
</article>
{% endwith %}
</div>
<div class="card ~neutral !low py-0 mb-6">
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data %}
<div class="card overflow-visible py-0 mb-6">
{% include 'dashboard/includes/time_chart.html' with data=stats.chart_data tooltip_format=stats.chart_tooltip_format granularity=stats.chart_granularity click_zoom=True %}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="card ~neutral !low limited-height py-2">
{% endif %}
<div id="card-grid" class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div class="card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
@@ -100,8 +109,20 @@
<tbody>
{% for location in stats.locations %}
<tr>
<td>{{location.location|default:"Unknown"|urldisplay}}</td>
<td class="rf">{{location.count|intcomma}}</td>
<td class="truncate w-full max-w-0 relative">
{% include 'dashboard/includes/bar.html' with count=location.count max=stats.locations.0.count total=stats.hit_count %}
<div class="relative flex items-center">
{{location.location|default:"Unknown"|urldisplay}}
</div>
</td>
<td>
<div class="flex justify-end items-center">
{{location.count|intcomma}}
<span class="text-xs rf min-w-48">
({{location.count|percent:stats.hit_count}})
</span>
</div>
</td>
</tr>
{% empty %}
<tr>
@@ -111,7 +132,55 @@
</tbody>
</table>
</div>
<div class="card ~neutral !low limited-height py-2">
<div class="geo-map card py-2 overflow-y-hidden">
<p class="text-sm font-semibold p-2 border-b mb-2" style="color: var(--color-title)">
Sessions by Geography &nbsp
<button onclick="document.getElementById('card-grid').classList.add('geo-card--use-table-view')" class="text-xs select-none p-0 text-urge-600">
(view table)
</button>
</p>
{% include 'dashboard/includes/map_chart.html' with countries=stats.countries %}
</div>
<div class="geo-table card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
<th>
Country &nbsp
<button onclick="document.getElementById('card-grid').classList.remove('geo-card--use-table-view'); geoMap.resize()" class="text-xs select-none p-0 text-urge-600">
(view map)
</button>
</th>
<th class="rf">Sessions</th>
</tr>
</thead>
<tbody>
{% for country in stats.countries %}
<tr>
<td class="truncate w-full max-w-0 relative" title="{{country.country|country_name}}">
{% include 'dashboard/includes/bar.html' with count=country.count max=stats.countries.0.count total=stats.session_count %}
<div class="relative flex items-center">
<span class="flex-none {{country.country|flag_class}}"></span> <span class="truncate">{{country.country|country_name}}</span>
</div>
</td>
<td>
<div class="flex justify-end items-center">
{{country.count|intcomma}}
<span class="text-xs rf min-w-48">
({{country.count|percent:stats.session_count}})
</span>
</div>
</td>
</tr>
{% empty %}
<tr>
<td><span class="text-gray-600">No data yet...</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
@@ -122,8 +191,20 @@
<tbody>
{% for referrer in stats.referrers %}
<tr>
<td>{{referrer.referrer|default:"Direct"|urldisplay}}</td>
<td class="rf">{{referrer.count|intcomma}}</td>
<td class="truncate w-full max-w-0 relative">
{% include 'dashboard/includes/bar.html' with count=referrer.count max=stats.referrers.0.count total=stats.session_count %}
<div class="relative flex items-center">
{{referrer.referrer|default:"Direct"|urldisplay}}
</div>
</td>
<td>
<div class="flex justify-end items-center">
{{referrer.count|intcomma}}
<span class="text-xs rf min-w-48">
({{referrer.count|percent:stats.session_count}})
</span>
</div>
</td>
</tr>
{% empty %}
<tr>
@@ -133,29 +214,7 @@
</tbody>
</table>
</div>
<div class="card ~neutral !low limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
<th>Country</th>
<th class="rf">Sessions</th>
</tr>
</thead>
<tbody>
{% for country in stats.countries %}
<tr>
<td>{{country.country|flag_emoji}} {{country.country|country_name}}</td>
<td class="rf">{{country.count|intcomma}}</td>
</tr>
{% empty %}
<tr>
<td><span class="text-gray-600">No data yet...</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card ~neutral !low limited-height py-2">
<div class="card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
@@ -166,8 +225,20 @@
<tbody>
{% for os in stats.operating_systems %}
<tr>
<td class="flex items-center">{{os.os|iconify}}<span>{{os.os|default:"Unknown"}}</span></td>
<td class="rf">{{os.count|intcomma}}</td>
<td class="flex items-center truncate w-full max-w-0 relative" title="{{os.os|default:'Unknown'}}">
{% include 'dashboard/includes/bar.html' with count=os.count max=stats.operating_systems.0.count total=stats.session_count %}
<div class="relative flex items-center">
{{os.os|iconify}}<span class="truncate">{{os.os|default:"Unknown"}}</span>
</div>
</td>
<td>
<div class="flex justify-end items-center">
{{os.count|intcomma}}
<span class="text-xs rf min-w-48">
({{os.count|percent:stats.session_count}})
</span>
</div>
</td>
</tr>
{% empty %}
<tr>
@@ -177,7 +248,7 @@
</tbody>
</table>
</div>
<div class="card ~neutral !low limited-height py-2">
<div class="card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
@@ -188,9 +259,21 @@
<tbody>
{% for browser in stats.browsers %}
<tr>
<td class="flex items-center">
{{browser.browser|iconify}}<span>{{browser.browser|default:"Unknown"}}</span></td>
<td class="rf">{{browser.count|intcomma}}</td>
<td class="flex items-center truncate w-full max-w-0 relative" title="{{browser.browser|default:'Unknown'}}">
{% include 'dashboard/includes/bar.html' with count=browser.count max=stats.browsers.0.count total=stats.session_count %}
</div>
<div class="relative flex items-center">
{{browser.browser|iconify}}<span class="truncate">{{browser.browser|default:"Unknown"}}</span>
</div>
</td>
<td>
<div class="flex justify-end items-center">
{{browser.count|intcomma}}
<span class="text-xs rf min-w-48">
({{browser.count|percent:stats.session_count}})
</span>
</div>
</td>
</tr>
{% empty %}
<tr>
@@ -200,7 +283,7 @@
</tbody>
</table>
</div>
<div class="card ~neutral !low limited-height py-2">
<div class="card limited-height py-2">
<table class="table">
<thead class="text-sm">
<tr>
@@ -211,8 +294,20 @@
<tbody>
{% for device_type in stats.device_types %}
<tr>
<td>{{device_type.device_type|default:"Unknown"|title}}</td>
<td class="rf">{{device_type.count|intcomma}}</td>
<td class="truncate w-full max-w-0 relative">
{% include 'dashboard/includes/bar.html' with count=device_type.count max=stats.device_types.0.count total=stats.session_count %}
<div class="relative flex items-center">
<span class="truncate">{{device_type.device_type|default:"Unknown"|title}}</span>
</div>
</td>
<td>
<div class="flex justify-end items-center">
{{device_type.count|intcomma}}
<span class="text-xs rf min-w-48">
({{device_type.count|percent:stats.session_count}})
</span>
</div>
</td>
</tr>
{% empty %}
<tr>
@@ -223,11 +318,11 @@
</table>
</div>
</div>
<div class="card ~neutral !low limited-height py-2">
<div class="card py-2 overflow-auto">
{% include 'dashboard/includes/session_list.html' %}
<hr class="sep h-8">
<a href="{% url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more
<hr class="sep h-8 md:h-12">
<a href="{% contextual_url 'dashboard:service_session_list' service.uuid %}" class="button ~neutral w-auto mb-2">View more
sessions
&rarr;</a>
</div>
{% endblock %}
{% endblock %}

View File

@@ -7,14 +7,14 @@
{% block content %}
<h4 class="heading leading-none">Create Service</h4>
<hr class="sep">
<form class="card ~neutral !low p-0 max-w-xl" method="POST">
<form class="card p-0 max-w-xl" method="POST">
{% csrf_token %}
<div class="p-4">
{% include 'dashboard/includes/service_form.html' %}
</div>
<div class="section ~urge !normal p-4">
<button type="submit" class="button ~urge !high">Create</button>
<a href="{% url 'dashboard:dashboard' %}" class="button ~urge !low">Cancel</a>
<div class="section ~urge @low p-4">
<button type="submit" class="button ~urge @high">Create</button>
<a href="{% url 'dashboard:dashboard' %}" class="ml-4 text-urge-600">Cancel</a>
</div>
</form>
{% endblock %}

View File

@@ -5,16 +5,16 @@
{% block head_title %}Delete {{object.name}}{% endblock %}
{% block service_content %}
<form class="card ~neutral !low p-0 max-w-xl" method="POST">
<form class="card p-0 max-w-xl" method="POST">
{% csrf_token %}
<div class="p-4">
<p>Are you sure you want to delete this service? All of its
analytics and associated data will be permanently deleted.</p>
{{form|a17t}}
</div>
<div class="section ~critical !normal p-4">
<button type="submit" class="button ~critical !high">Delete</button>
<a href="{% url 'dashboard:service' object.uuid %}" class="button ~critical !low">Cancel</a>
<div class="section ~critical @low p-4">
<button type="submit" class="button ~critical @high">Delete</button>
<a href="{% url 'dashboard:service' object.uuid %}" class="ml-4 text-critical-600">Cancel</a>
</div>
</form>
{% endblock %}

View File

@@ -5,11 +5,11 @@
{% block head_title %}{{object.name}} Session{% endblock %}
{% block service_actions %}
<a href="{% url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">Analytics &rarr;</a>
<a href="{% contextual_url 'dashboard:service' object.uuid %}" class="button ~neutral @low w-auto">Analytics &rarr;</a>
{% endblock %}
{% block service_content %}
<article class="card ~neutral !high">
<article class="card ~neutral @high">
<div class="md:flex items-center justify-between">
<div>
<h3 class="heading text-2xl mr-4">
@@ -18,10 +18,10 @@
</div>
<div>
<p class="font-medium text-lg">{{session.start_time|date:"M j Y, g:i a"}} to
{{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>
<hr class="sep h-8">
<hr class="sep h-8 md:h-12">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-gray-400 font-medium">
<div>
<p>Browser</p>
@@ -45,13 +45,13 @@
</div>
<div>
<p>Country</p>
<p class="label">{{session.country|flag_emoji}} {{session.country|country_name}}</p>
<p class="label"><span class="{{session.country|flag_class}}"></span>{{session.country|country_name}}</p>
</div>
<div>
<p>Location</p>
<p class="label">
{% if session.latitude %}
<a href="https://www.google.com/maps/search/?api=1&query={{session.latitude}},{{session.longitude}}">Open
<a href="{{session|location_url}}" target="_blank">Open
in Maps &nearr;</a>
{% else %}
Unknown
@@ -70,9 +70,9 @@
<div class="md:w-2/12 mb-2 md:mr-4 pt-4 md:text-right">
<div class="text-lg font-medium">{{hit.start_time|date:"g:i a"}}</div>
</div>
<div class="md:flex card ~neutral !low flex-grow justify-between">
<div class="md:flex card flex-grow justify-between">
<div class="mb-4 md:mb-0 md:w-1/2">
<p class="label font-medium text-lg">{{hit.location|default:"Unknown"|urlize}}</p>
<p class="label font-medium text-lg truncate">{{hit.location|default:"Unknown"|urlize}}</p>
{% if hit.referrer %}
<p>via {{hit.referrer|urlize}}<p>
{% endif %}

View File

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

View File

@@ -5,35 +5,30 @@
{% block head_title %}{{object.name}} Management{% endblock %}
{% block service_actions %}
<a href="{% url 'dashboard:service' object.uuid %}" class="button field bg-neutral-000 w-auto">View &rarr;</a>
<a href="{% url 'dashboard:service' object.uuid %}" class="button ~neutral @low w-auto">View &rarr;</a>
{% endblock %}
{% block service_content %}
<div class="max-w-xl content">
<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>
<div class="card ~neutral !high font-mono text-sm">
{% filter force_escape %}<noscript><img
src="{{script_protocol}}{{request.site.domain}}{% url 'ingress:endpoint_pixel' object.uuid %}"></noscript>
<script src="{{script_protocol}}{{request.site.domain}}{% url 'ingress:endpoint_script' object.uuid %}"></script>
{% endfilter %}
</div>
{% include 'dashboard/includes/service_snippet.html' %}
<hr class="sep h-4">
<h5>Settings</h5>
<form class="card ~neutral !low p-0" method="POST">
<form class="card p-0" method="POST">
{% csrf_token %}
<div class="p-4">
{% include 'dashboard/includes/service_form.html' %}
</div>
<div class="section ~neutral !normal p-4 flex justify-between">
<div class="section ~neutral @low p-4 flex justify-between">
<div>
<button type="submit" class="button ~neutral !high">Save</button>
<a href="{% url 'dashboard:service' object.uuid %}" class="button ~neutral !low">Cancel</a>
<button type="submit" class="button ~neutral @high">Save</button>
<a href="{% url 'dashboard:service' object.uuid %}" class="button ~neutral @low">Cancel</a>
</div>
<div>
<a href="{% url 'dashboard:service_delete' object.uuid %}" class="button ~critical !high">Delete</a>
<a href="{% url 'dashboard:service_delete' object.uuid %}" class="button ~critical @high">Delete</a>
</div>
</div>
</form>
</div>
{% endblock %}
{% endblock %}

View File

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

View File

@@ -1,12 +1,14 @@
from datetime import timedelta
from urllib.parse import urlparse
import urllib
import flag
import pycountry
from django import template
from django.conf import settings
from django.utils import timezone
from django.utils.html import escape
from django.utils.safestring import SafeString
from django.template.defaulttags import url as url_tag
register = template.Library()
@@ -26,11 +28,11 @@ def naturaldelta(timedelta):
@register.filter
def flag_emoji(isocode):
try:
return flag.flag(isocode)
except:
return ""
def flag_class(isocode):
if isocode:
return "mr-1 flag-icon flag-icon-" + isocode.lower()
else:
return "hidden"
@register.filter
@@ -41,9 +43,22 @@ def country_name(isocode):
return "Unknown"
@register.filter
def datamap_id(isocode):
try:
return pycountry.countries.get(alpha_2=isocode).alpha_3
except:
return "UNKNOWN"
@register.simple_tag
def relative_stat_tone(
start, end, good="UP", good_classes=None, bad_classes=None, neutral_classes=None,
start,
end,
good="UP",
good_classes=None,
bad_classes=None,
neutral_classes=None,
):
good_classes = good_classes or "~positive"
bad_classes = bad_classes or "~critical"
@@ -61,28 +76,25 @@ def relative_stat_tone(
@register.simple_tag
def percent_change_display(start, end):
try:
if start == None or end == None:
return SafeString("&Delta; n/a")
if start == end:
direction = "&Delta; "
else:
direction = "&uarr; " if end > start else "&darr; "
if start == None or end == None:
return SafeString("&Delta; n/a")
if start == end:
direction = "&Delta; "
else:
direction = "&uarr; " if end > start else "&darr; "
if start == 0 and end != 0:
pct_change = "100%"
elif start == 0:
pct_change = "0%"
if start == 0 and end != 0:
pct_change = "100%"
elif start == 0:
pct_change = "0%"
else:
change = int(round(100 * abs(end - start) / max(start, 1)))
if change > 999:
return "> 999%"
else:
change = int(round(100 * abs(end - start) / max(start, 1)))
if change > 999:
return "> 999%"
else:
pct_change = str(change) + "%"
pct_change = str(change) + "%"
return SafeString(direction + pct_change)
except: # TODO: filter for specific issues
return SafeString("&Delta; ?")
return SafeString(direction + pct_change)
@register.inclusion_tag("dashboard/includes/sidebar_footer.html")
@@ -100,6 +112,12 @@ def compare(
bad_classes=None,
neutral_classes=None,
):
if isinstance(start, timedelta):
start = start.seconds
if isinstance(end, timedelta):
end = end.seconds
return {
"start": start,
"end": end,
@@ -162,7 +180,7 @@ def iconify(text):
domain = text + ".com"
return SafeString(
f'<span class="icon mr-1"><img src="https://icons.duckduckgo.com/ip3/{domain}.ico"></span>'
f'<span class="icon mr-1 flex-none"><img src="https://icons.duckduckgo.com/ip3/{domain}.ico"></span>'
)
@@ -171,7 +189,87 @@ def urldisplay(url):
if url.startswith("http"):
display_url = url.replace("http://", "").replace("https://", "")
return SafeString(
f"<a href='{url}' title='{url}' rel='nofollow' class='flex items-center'>{iconify(url)} {escape(display_url if len(display_url) < 40 else display_url[:40] + '...')}</a>"
f"<a href='{url}' title='{url}' rel='nofollow' class='flex items-center mr-1 truncate'>{iconify(url)}<span class='truncate'>{escape(display_url)}</span></a>"
)
else:
return url
class ContextualURLNode(template.Node):
"""Extension of the Django URLNode to support including contextual parameters in URL outputs. In other words, URLs generated will keep the start and end date parameters."""
CONTEXT_PARAMS = ["startDate", "endDate"]
def __init__(self, urlnode):
self.urlnode = urlnode
def __repr__(self):
return self.urlnode.__repr__()
def render(self, context):
url = self.urlnode.render(context)
if self.urlnode.asvar:
url = context[self.urlnode.asvar]
url_parts = list(urlparse(url))
query = dict(urllib.parse.parse_qsl(url_parts[4]))
query.update(
{
param: context.request.GET.get(param)
for param in self.CONTEXT_PARAMS
if param in context.request.GET and param not in query
}
)
url_parts[4] = urllib.parse.urlencode(query)
url_final = urllib.parse.urlunparse(url_parts)
if self.urlnode.asvar:
context[self.urlnode.asvar] = url_final
return ""
else:
return url_final
@register.tag
def contextual_url(*args, **kwargs):
urlnode = url_tag(*args, **kwargs)
return ContextualURLNode(urlnode)
@register.filter
def location_url(session):
return settings.LOCATION_URL.replace("$LATITUDE", str(session.latitude)).replace(
"$LONGITUDE", str(session.longitude)
)
@register.filter
def percent(value, total):
if total == 0:
return "N/A"
percent = value / total
if percent < 0.001:
return "<0.1%"
return f"{percent:.1%}"
@register.simple_tag
def bar_width(count, max, total):
if total == 0 or max == 0:
return "0"
if settings.USE_RELATIVE_MAX_IN_BAR_VISUALIZATION:
percent = count / max
else:
percent = count / total
if percent < 0.001:
return "0"
return f"{percent:.1%}"

View File

View File

@@ -0,0 +1,43 @@
from django.test import TestCase, RequestFactory
from django.conf import settings
from django.urls import reverse
from core.factories import UserFactory
from dashboard.views import DashboardView
class QuestionModelTests(TestCase):
def setUp(self):
# Every test needs access to the request factory.
self.factory = RequestFactory()
self.user = UserFactory()
def tearDown(self):
pass
def tests_unauthenticated_dashboard_view(self):
"""
GIVEN: Unauthenticated user
WHEN: Accessing the dashboard view
THEN: It's redirected to login page with NEXT url to dashboard
"""
login_url = settings.LOGIN_URL
response = self.client.get(reverse("dashboard:dashboard"))
self.assertEqual(response.status_code, 302)
self.assertEqual(
response.url, f"{login_url}?next={reverse('dashboard:dashboard')}"
)
def tests_authenticated_dashboard_view(self):
"""
GIVEN: Authenticated user
WHEN: Accessing the dashboard view
THEN: It should respond with 200 and render the view
"""
request = self.factory.get(reverse("dashboard:dashboard"))
request.user = self.user
# Use this syntax for class-based views.
response = DashboardView.as_view()(request)
self.assertEqual(response.status_code, 200)

View File

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

View File

@@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/2.2/ref/settings/
"""
import os
from dotenv import load_dotenv
# import module sys to get the type of exception
import sys
@@ -17,8 +18,11 @@ import urllib.parse as urlparse
# Messages
from django.contrib.messages import constants as messages
# Load environment variables
load_dotenv()
# Increment on new releases
VERSION = "v0.7.3"
VERSION = "0.11.0"
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -58,6 +62,7 @@ INSTALLED_APPS = [
"allauth",
"allauth.account",
"allauth.socialaccount",
"debug_toolbar",
]
MIDDLEWARE = [
@@ -70,6 +75,7 @@ MIDDLEWARE = [
"django.contrib.sites.middleware.CurrentSiteMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
ROOT_URLCONF = "shynet.urls"
@@ -145,9 +151,15 @@ AUTH_PASSWORD_VALIDATORS = [
{
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",},
{
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
},
{
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
},
{
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
},
]
# Logging
@@ -247,6 +259,10 @@ LOGIN_REDIRECT_URL = "/"
SITE_ID = 1
INTERNAL_IPS = [
"127.0.0.1",
]
# Celery
CELERY_TASK_ALWAYS_EAGER = os.getenv("CELERY_TASK_ALWAYS_EAGER", "True") == "True"
@@ -282,18 +298,29 @@ else:
EMAIL_USE_SSL = os.environ.get("EMAIL_USE_SSL")
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS")
# Auto fields
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# NPM
NPM_ROOT_PATH = "../"
NPM_FILE_PATTERNS = {
"a17t": [os.path.join("dist", "a17t.css"), os.path.join("dist", "tailwind.css")],
"apexcharts": [os.path.join("dist", "apexcharts.min.js")],
"litepicker": [os.path.join("dist", "js", "main.js")],
"turbolinks": [os.path.join("dist", "turbolinks.js")],
"stimulus": [os.path.join("dist", "stimulus.umd.js")],
"litepicker": [
os.path.join("dist", "nocss", "litepicker.js"),
os.path.join("dist", "css", "litepicker.css"),
os.path.join("dist", "plugins", "ranges.js"),
],
"inter-ui": [os.path.join("Inter (web)", "*")],
"@fortawesome": [os.path.join("fontawesome-free", "js", "all.min.js")],
"datamaps": [os.path.join("dist", "datamaps.world.min.js")],
"d3": ["d3.min.js"],
"topojson": [os.path.join("build", "topojson.min.js")],
"flag-icon-css": [
os.path.join("css", "flag-icon.min.css"),
os.path.join("flags", "*"),
],
}
# Shynet
@@ -321,3 +348,22 @@ SHOW_SHYNET_VERSION = os.getenv("SHOW_SHYNET_VERSION", "True") == "True"
# Should Shynet show third-party icons in the dashboard?
SHOW_THIRD_PARTY_ICONS = os.getenv("SHOW_THIRD_PARTY_ICONS", "True") == "True"
# Should Shynet never collect any IP?
BLOCK_ALL_IPS = os.getenv("BLOCK_ALL_IPS", "False") == "True"
# Include date and service ID in salt?
AGGRESSIVE_HASH_SALTING = os.getenv("AGGRESSIVE_HASH_SALTING", "False") == "True"
# What location url should be linked to in the frontend?
LOCATION_URL = os.getenv(
"LOCATION_URL", "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE"
)
# How many services should be displayed on dashboard page?
DASHBOARD_PAGE_SIZE = int(os.getenv("DASHBOARD_PAGE_SIZE", "5"))
# Should background bars be scaled to full width?
USE_RELATIVE_MAX_IN_BAR_VISUALIZATION = (
os.getenv("USE_RELATIVE_MAX_IN_BAR_VISUALIZATION", "True") == "True"
)

View File

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

View File

@@ -16,9 +16,6 @@ if [[ ${startup_results[1]} == True ]]; then
echo "Warning: no admin user available. Consult docs for instructions."
fi
if [[ ${startup_results[2]} == True ]]; then
echo "Warning: Shynet's hostname is not set. The script won't work correctly. Consult docs for instructions."
fi
if [[ ${startup_results[3]} == True ]]; then
echo "Warning: Shynet's whitelabel is not set. Consult docs for instructions."
fi
echo "Startup checks complete!"

22
tailwind.config.js Normal file
View File

@@ -0,0 +1,22 @@
let colors = require("tailwindcss/colors")
module.exports = {
content: ["./**/*.{html,py}"],
theme: {
extend: {
colors: {
neutral: colors.slate,
positive: colors.green,
urge: colors.violet,
warning: colors.yellow,
info: colors.blue,
critical: colors.red,
inf: "white",
zero: colors.slate[900]
}
},
},
plugins: [
require("a17t")
],
}

View File

@@ -3,11 +3,14 @@
<html>
<head>
<title>Pixel test</title>
<title>JS test</title>
</head>
<body>
<script src="http://localhost:8000/ingress/9b2c4e2f-8d29-4418-82d4-b68e06795025/script.js"></script>
<noscript>
<img src="http://localhost:8000/ingress/0ca733e8-c41f-462b-a11a-4ba0cea29948/pixel.gif">
</noscript>
<script defer src="http://localhost:8000/ingress/0ca733e8-c41f-462b-a11a-4ba0cea29948/script.js"></script>
</body>
</html>