Compare commits
60 Commits
haaavk/mas
...
v0.9.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff97a46fd9 | ||
|
|
afb78dc499 | ||
|
|
a4eaef0117 | ||
|
|
7891866214 | ||
|
|
eedcbc4e85 | ||
|
|
0d006620dd | ||
|
|
0b78f6df72 | ||
|
|
74ddef1670 | ||
|
|
9d9d4a7b1e | ||
|
|
d66f683104 | ||
|
|
b44642e023 | ||
|
|
c12a7e9e71 | ||
|
|
0294d31ea4 | ||
|
|
40cb5afbad | ||
|
|
073bd94112 | ||
|
|
3a01fefcff | ||
|
|
14a7ec68f3 | ||
|
|
fdf2ab719b | ||
|
|
737eeb5df4 | ||
|
|
cb4855e4fc | ||
|
|
f4127cf9b1 | ||
|
|
159015de1c | ||
|
|
a7548d7eba | ||
|
|
da87ddb18f | ||
|
|
4a76ab32fc | ||
|
|
4afeced7d3 | ||
|
|
2a6efe1b7f | ||
|
|
07f3926a9c | ||
|
|
14ed0b7979 | ||
|
|
ab51089647 | ||
|
|
86695dbcc4 | ||
|
|
4e4cfe081b | ||
|
|
f54b67ef0f | ||
|
|
43f339e32b | ||
|
|
b144efaa9b | ||
|
|
c06b7a094a | ||
|
|
f13745f15e | ||
|
|
a1a083a403 | ||
|
|
8b167b2c74 | ||
|
|
4cd0c4735d | ||
|
|
d9e1ffddb1 | ||
|
|
9fb875f749 | ||
|
|
f6e502dfbd | ||
|
|
7c69b0bd81 | ||
|
|
78bea501a8 | ||
|
|
c2daf3a5a5 | ||
|
|
df6786e037 | ||
|
|
6621625d90 | ||
|
|
32ae0aa5f3 | ||
|
|
2221a99662 | ||
|
|
69ec37331a | ||
|
|
ea893b2322 | ||
|
|
e9536f1816 | ||
|
|
6f835a4f27 | ||
|
|
faf4f48e75 | ||
|
|
278306daa4 | ||
|
|
2c0fafefea | ||
|
|
6eb41e016a | ||
|
|
369f4d8d6b | ||
|
|
3d43f223eb |
46
.github/workflows/build-docker.yml
vendored
Normal file
46
.github/workflows/build-docker.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
name: Build docker images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish_to_docker_hub:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Prepare tags
|
||||||
|
id: prep
|
||||||
|
run: |
|
||||||
|
DOCKER_IMAGE=milesmcc/shynet
|
||||||
|
VERSION=${GITHUB_REF#refs/tags/}
|
||||||
|
TAGS="${DOCKER_IMAGE}:${VERSION},${DOCKER_IMAGE}:latest"
|
||||||
|
echo ::set-output name=tags::${TAGS}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push advanced image
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.prep.outputs.tags }}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM python:3-alpine
|
FROM python:alpine3.12
|
||||||
|
|
||||||
# Getting things ready
|
# Getting things ready
|
||||||
WORKDIR /usr/src/shynet
|
WORKDIR /usr/src/shynet
|
||||||
|
|||||||
@@ -92,3 +92,7 @@ AGGRESSIVE_HASH_SALTING=True
|
|||||||
# - https://www.google.com/maps/search/?api=1&query=$LATITUDE,$LONGITUDE
|
# - https://www.google.com/maps/search/?api=1&query=$LATITUDE,$LONGITUDE
|
||||||
# - https://www.mapquest.com/near-$LATITUDE,$LONGITUDE
|
# - https://www.mapquest.com/near-$LATITUDE,$LONGITUDE
|
||||||
LOCATION_URL=https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$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
|
||||||
|
|||||||
5
app.json
5
app.json
@@ -127,6 +127,11 @@
|
|||||||
"description": "Custom location url to link to in frontend.",
|
"description": "Custom location url to link to in frontend.",
|
||||||
"value": "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE",
|
"value": "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE",
|
||||||
"required": false
|
"required": false
|
||||||
|
},
|
||||||
|
"DASHBOARD_PAGE_SIZE": {
|
||||||
|
"description": "How many services should be displayed on dashboard page?",
|
||||||
|
"value": "5",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,14 @@
|
|||||||
var Shynet = {
|
var Shynet = {
|
||||||
idempotency: null,
|
idempotency: null,
|
||||||
heartbeatTaskId: null,
|
heartbeatTaskId: null,
|
||||||
|
skipHeartbeat: false,
|
||||||
sendHeartbeat: function () {
|
sendHeartbeat: function () {
|
||||||
try {
|
try {
|
||||||
if (document.hidden) {
|
if (document.hidden || Shynet.skipHeartbeat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Shynet.skipHeartbeat = true;
|
||||||
var xhr = new XMLHttpRequest();
|
var xhr = new XMLHttpRequest();
|
||||||
xhr.open(
|
xhr.open(
|
||||||
"POST",
|
"POST",
|
||||||
@@ -20,6 +23,12 @@ var Shynet = {
|
|||||||
true
|
true
|
||||||
);
|
);
|
||||||
xhr.setRequestHeader("Content-Type", "application/json");
|
xhr.setRequestHeader("Content-Type", "application/json");
|
||||||
|
xhr.onload = function () {
|
||||||
|
Shynet.skipHeartbeat = false;
|
||||||
|
};
|
||||||
|
xhr.onerror = function () {
|
||||||
|
Shynet.skipHeartbeat = false;
|
||||||
|
};
|
||||||
xhr.send(
|
xhr.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
idempotency: Shynet.idempotency,
|
idempotency: Shynet.idempotency,
|
||||||
@@ -30,13 +39,14 @@ var Shynet = {
|
|||||||
window.performance.timing.navigationStart,
|
window.performance.timing.navigationStart,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} catch (e) { }
|
} catch (e) {}
|
||||||
},
|
},
|
||||||
newPageLoad: function () {
|
newPageLoad: function () {
|
||||||
if (Shynet.heartbeatTaskId != null) {
|
if (Shynet.heartbeatTaskId != null) {
|
||||||
clearInterval(Shynet.heartbeatTaskId);
|
clearInterval(Shynet.heartbeatTaskId);
|
||||||
}
|
}
|
||||||
Shynet.idempotency = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
Shynet.idempotency = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
||||||
|
Shynet.skipHeartbeat = false;
|
||||||
Shynet.heartbeatTaskId = setInterval(Shynet.sendHeartbeat, parseInt("{{heartbeat_frequency}}"));
|
Shynet.heartbeatTaskId = setInterval(Shynet.sendHeartbeat, parseInt("{{heartbeat_frequency}}"));
|
||||||
Shynet.sendHeartbeat();
|
Shynet.sendHeartbeat();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ from django.conf import settings
|
|||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models.functions import TruncDate
|
from django.db.models.functions import TruncDate, TruncHour
|
||||||
from django.db.utils import NotSupportedError
|
from django.db.utils import NotSupportedError
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# How long a session a needs to go without an update to no longer be considered 'active' (i.e., currently online)
|
||||||
ACTIVE_USER_TIMEDELTA = timezone.timedelta(
|
ACTIVE_USER_TIMEDELTA = timezone.timedelta(
|
||||||
milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2
|
milliseconds=settings.SCRIPT_HEARTBEAT_FREQUENCY * 2
|
||||||
)
|
)
|
||||||
@@ -125,8 +125,10 @@ class Service(models.Model):
|
|||||||
Session = apps.get_model("analytics", "Session")
|
Session = apps.get_model("analytics", "Session")
|
||||||
Hit = apps.get_model("analytics", "Hit")
|
Hit = apps.get_model("analytics", "Hit")
|
||||||
|
|
||||||
|
tz_now = timezone.now()
|
||||||
|
|
||||||
currently_online = Session.objects.filter(
|
currently_online = Session.objects.filter(
|
||||||
service=self, last_seen__gt=timezone.now() - ACTIVE_USER_TIMEDELTA
|
service=self, last_seen__gt=tz_now - ACTIVE_USER_TIMEDELTA
|
||||||
).count()
|
).count()
|
||||||
|
|
||||||
sessions = Session.objects.filter(
|
sessions = Session.objects.filter(
|
||||||
@@ -210,17 +212,35 @@ class Service(models.Model):
|
|||||||
if session_count == 0:
|
if session_count == 0:
|
||||||
avg_session_duration = None
|
avg_session_duration = None
|
||||||
|
|
||||||
session_chart_data = {
|
# Show hourly chart for date ranges of 3 days or less, otherwise daily chart
|
||||||
k["date"]: k["count"]
|
if (end_time - start_time).days < 3:
|
||||||
for k in sessions.annotate(date=TruncDate("start_time"))
|
session_chart_tooltip_format = "MM/dd HH:mm"
|
||||||
.values("date")
|
session_chart_granularity = "hourly"
|
||||||
.annotate(count=models.Count("uuid"))
|
session_chart_data = {
|
||||||
.order_by("date")
|
k["hour"]: k["count"]
|
||||||
}
|
for k in sessions.annotate(hour=TruncHour("start_time"))
|
||||||
for day_offset in range((end_time - start_time).days + 1):
|
.values("hour")
|
||||||
day = (start_time + timezone.timedelta(days=day_offset)).date()
|
.annotate(count=models.Count("uuid"))
|
||||||
if day not in session_chart_data:
|
.order_by("hour")
|
||||||
session_chart_data[day] = 0
|
}
|
||||||
|
for hour_offset in range(int((end_time - start_time).total_seconds() / 3600) + 1):
|
||||||
|
hour = (start_time + timezone.timedelta(hours=hour_offset))
|
||||||
|
if hour not in session_chart_data:
|
||||||
|
session_chart_data[hour] = 0 if hour <= tz_now else None
|
||||||
|
else:
|
||||||
|
session_chart_tooltip_format = "MMM d"
|
||||||
|
session_chart_granularity = "daily"
|
||||||
|
session_chart_data = {
|
||||||
|
k["date"]: k["count"]
|
||||||
|
for k in sessions.annotate(date=TruncDate("start_time"))
|
||||||
|
.values("date")
|
||||||
|
.annotate(count=models.Count("uuid"))
|
||||||
|
.order_by("date")
|
||||||
|
}
|
||||||
|
for day_offset in range((end_time - start_time).days + 1):
|
||||||
|
day = (start_time + timezone.timedelta(days=day_offset)).date()
|
||||||
|
if day not in session_chart_data:
|
||||||
|
session_chart_data[day] = 0 if day <= tz_now.date() else None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"currently_online": currently_online,
|
"currently_online": currently_online,
|
||||||
@@ -249,6 +269,8 @@ class Service(models.Model):
|
|||||||
)
|
)
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
"session_chart_tooltip_format": session_chart_tooltip_format,
|
||||||
|
"session_chart_granularity": session_chart_granularity,
|
||||||
"online": True,
|
"online": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<hr class="sep h-4">
|
<hr class="sep h-4">
|
||||||
<div style="bottom: -1px;">
|
<div style="bottom: -1px;">
|
||||||
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data sparkline=True height=100 name=object.uuid %}
|
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data sparkline=True height=100 name=object.uuid tooltip_format=stats.session_chart_tooltip_format granularity=stats.session_chart_granularity %}
|
||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</a>
|
</a>
|
||||||
@@ -6,6 +6,9 @@
|
|||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
shared: false,
|
shared: false,
|
||||||
|
x: {
|
||||||
|
format: '{{tooltip_format|default:"MMM d"}}',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
colors: ["#805AD5"],
|
colors: ["#805AD5"],
|
||||||
chart: {
|
chart: {
|
||||||
@@ -34,6 +37,14 @@
|
|||||||
stops: [0, 75, 100]
|
stops: [0, 75, 100]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{% if granularity == "daily" and click_zoom %}
|
||||||
|
events: {
|
||||||
|
markerClick: function(event, chartContext, { seriesIndex, dataPointIndex, w: {config}}) {
|
||||||
|
const day = config.series[seriesIndex].data[dataPointIndex].x
|
||||||
|
window.location.href = `?startDate=${day}&endDate=${day}`
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{% endif %}
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
padding: {
|
padding: {
|
||||||
@@ -63,6 +74,9 @@
|
|||||||
},
|
},
|
||||||
xaxis: {
|
xaxis: {
|
||||||
type: "datetime",
|
type: "datetime",
|
||||||
|
labels: {
|
||||||
|
datetimeUTC: false
|
||||||
|
},
|
||||||
},
|
},
|
||||||
stroke: {
|
stroke: {
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
|
|||||||
@@ -94,7 +94,7 @@
|
|||||||
{% endwith %}
|
{% endwith %}
|
||||||
</div>
|
</div>
|
||||||
<div class="card overflow-visible ~neutral !low py-0 mb-6">
|
<div class="card overflow-visible ~neutral !low py-0 mb-6">
|
||||||
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data %}
|
{% include 'dashboard/includes/time_chart.html' with data=stats.session_chart_data tooltip_format=stats.session_chart_tooltip_format granularity=stats.session_chart_granularity click_zoom=True %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ from .mixins import DateRangeMixin
|
|||||||
class DashboardView(LoginRequiredMixin, DateRangeMixin, ListView):
|
class DashboardView(LoginRequiredMixin, DateRangeMixin, ListView):
|
||||||
model = Service
|
model = Service
|
||||||
template_name = "dashboard/pages/dashboard.html"
|
template_name = "dashboard/pages/dashboard.html"
|
||||||
paginate_by = 5
|
paginate_by = settings.DASHBOARD_PAGE_SIZE
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return Service.objects.filter(
|
return Service.objects.filter(
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import urllib.parse as urlparse
|
|||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
|
|
||||||
# Increment on new releases
|
# Increment on new releases
|
||||||
VERSION = "v0.8.2"
|
VERSION = "v0.9.1"
|
||||||
|
|
||||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
@@ -347,3 +347,6 @@ AGGRESSIVE_HASH_SALTING = os.getenv("AGGRESSIVE_HASH_SALTING", "False") == "True
|
|||||||
|
|
||||||
# What location url should be linked to in the frontend?
|
# What location url should be linked to in the frontend?
|
||||||
LOCATION_URL = os.getenv("LOCATION_URL", "https://www.openstreetmap.org/?mlat=$LATITUDE&mlon=$LONGITUDE")
|
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"))
|
||||||
|
|||||||
Reference in New Issue
Block a user