Compare commits

...

2 Commits

Author SHA1 Message Date
Jonas Schäfer
b40a625283 admin: allow disabling display of metrics
This is useful in situations where the admins of the Snikket
server (i.e. those who care for the docker containers) are not the
same people as the people who are admins of the Snikket service
(i.e. those who care for the users).
2021-05-27 17:59:40 +02:00
Jonas Schäfer
8a293985ca Implement system status panel
This offers system metrics and a way to send a broadcast
message to all online or registered users.

Requires prosody-modules cade5dac1003.
2021-05-27 17:21:58 +02:00
9 changed files with 514 additions and 61 deletions

View File

@@ -160,6 +160,7 @@ class AppConfig:
# Future versions may change this default, and the standard deployment # Future versions may change this default, and the standard deployment
# tools may also very well override it. # tools may also very well override it.
max_avatar_size = environ.var(1024*1024, converter=int) max_avatar_size = environ.var(1024*1024, converter=int)
show_metrics = environ.bool_var(True)
_UPPER_CASE = "".join(map(chr, range(ord("A"), ord("Z")+1))) _UPPER_CASE = "".join(map(chr, range(ord("A"), ord("Z")+1)))
@@ -191,6 +192,7 @@ def create_app() -> quart.Quart:
app.config["AVATAR_CACHE_TTL"] = config.avatar_cache_ttl app.config["AVATAR_CACHE_TTL"] = config.avatar_cache_ttl
app.config["APPLE_STORE_URL"] = config.apple_store_url app.config["APPLE_STORE_URL"] = config.apple_store_url
app.config["MAX_AVATAR_SIZE"] = config.max_avatar_size app.config["MAX_AVATAR_SIZE"] = config.max_avatar_size
app.config["SHOW_METRICS"] = config.show_metrics
app.context_processor(proc) app.context_processor(proc)
app.register_error_handler( app.register_error_handler(

View File

@@ -1,4 +1,6 @@
import json import json
import resource
import time
import typing import typing
from datetime import datetime from datetime import datetime
@@ -18,11 +20,12 @@ from quart import (
request, request,
abort, abort,
flash, flash,
current_app,
) )
from flask_babel import lazy_gettext as _l, _ from flask_babel import lazy_gettext as _l, _
from . import prosodyclient from . import prosodyclient, _version
from .infra import client, circle_name, BaseForm from .infra import client, circle_name, BaseForm
bp = Blueprint("admin", __name__, url_prefix="/admin") bp = Blueprint("admin", __name__, url_prefix="/admin")
@@ -31,7 +34,11 @@ bp = Blueprint("admin", __name__, url_prefix="/admin")
@bp.route("/") @bp.route("/")
@client.require_admin_session() @client.require_admin_session()
async def index() -> str: async def index() -> str:
return await render_template("admin_home.html") show_metrics = current_app.config["SHOW_METRICS"]
return await render_template(
"admin_home.html",
show_metrics=show_metrics,
)
class PasswordResetLinkPost(BaseForm): class PasswordResetLinkPost(BaseForm):
@@ -550,3 +557,148 @@ async def edit_circle(id_: str) -> typing.Union[str, quart.Response]:
circle_members=circle_members, circle_members=circle_members,
invite_form=invite_form, invite_form=invite_form,
) )
_CPU_EPOCH = time.process_time()
_MONOTONIC_EPOCH = time.monotonic()
def get_system_stats() -> typing.MutableMapping[
str,
typing.Optional[typing.Union[int, float]]]:
pagesize = resource.getpagesize()
my_rss: typing.Optional[int] = None
try:
with open("/proc/self/statm") as f:
stats = f.read().split()
my_rss = int(stats[1]) * pagesize
except (ValueError, IndexError, TypeError, OSError):
pass
my_cpu = (
(time.process_time() - _CPU_EPOCH) /
(time.monotonic() - _MONOTONIC_EPOCH)
)
mem_total, mem_available = None, None
load5: typing.Optional[float] = None
try:
with open("/proc/loadavg") as f:
stats = f.read().split()
load5 = float(stats[1])
except (ValueError, IndexError, TypeError, OSError):
pass
try:
with open("/proc/meminfo") as f:
for line in f:
if line.startswith("MemTotal"):
mem_total = int(line.split()[1]) * 1024
elif line.startswith("MemAvailable"):
mem_available = int(line.split()[1]) * 1024
if mem_total is not None and mem_available is not None:
break
except (ValueError, TypeError, IndexError, OSError):
pass
return {
"portal_rss": my_rss,
"portal_cpu": my_cpu,
"load5": load5,
"mem_total": mem_total,
"mem_available": mem_available,
}
class AnnouncementForm(BaseForm):
text = wtforms.StringField(
_("Message contents"),
widget=wtforms.widgets.TextArea(),
validators=[wtforms.validators.DataRequired()],
)
online_only = wtforms.BooleanField(
_("Only send to online users"),
)
action_post_all = wtforms.SubmitField(
_("Post to all users"),
)
action_send_preview = wtforms.SubmitField(
_("Send preview to yourself"),
)
@bp.route("/system/", methods=["GET", "POST"])
@client.require_admin_session()
async def system() -> typing.Union[str, quart.Response]:
form = AnnouncementForm()
if form.validate_on_submit():
recipients = "self"
if form.action_post_all.data:
if form.online_only.data:
recipients = "online"
else:
recipients = "all"
await client.post_announcement(
form.text.data,
recipients=recipients,
)
await flash(
_("Announcement sent!"),
"success",
)
if recipients != "self":
# redirect only if not previewing
return redirect(url_for(".system"))
version = None
now = None
show_metrics = current_app.config["SHOW_METRICS"]
if show_metrics:
version = await client.get_server_version()
now = time.time()
try:
prosody_metrics = await client.get_system_metrics()
except quart.exceptions.NotFound:
# server does not offer the endpoint for whatever reason -- ignore
prosody_metrics = {}
metrics = get_system_stats()
try:
prosody_cpu_metrics = prosody_metrics["cpu"]
except KeyError:
pass
else:
metrics["prosody_cpu"] = (prosody_cpu_metrics["value"] /
(now - prosody_cpu_metrics["since"]))
try:
metrics["prosody_rss"] = prosody_metrics["memory"]
except KeyError:
pass
try:
metrics["prosody_devices"] = prosody_metrics["c2s"]
except KeyError:
pass
for k in list(metrics.keys()):
if metrics[k] is None:
# so that defaulting in jinja works
del metrics[k]
else:
metrics = {}
return await render_template(
"admin_system.html",
metrics=metrics,
version=_version.version,
prosody_version=version,
form=form,
show_metrics=show_metrics,
)

View File

@@ -1,5 +1,6 @@
import base64 import base64
import itertools import itertools
import math
import secrets import secrets
import typing import typing
@@ -22,6 +23,15 @@ client.default_login_redirect = "main.login"
babel = flask_babel.Babel() babel = flask_babel.Babel()
BYTE_UNIT_SCALE_MAP = [
"B",
"kiB",
"MiB",
"GiB",
"TiB",
]
@babel.localeselector # type:ignore @babel.localeselector # type:ignore
def selected_locale() -> str: def selected_locale() -> str:
selected = request.accept_languages.best_match( selected = request.accept_languages.best_match(
@@ -42,12 +52,27 @@ def circle_name(c: typing.Any) -> str:
return c.name return c.name
def format_bytes(n: float) -> str:
scale = math.floor(math.log(n, 1024))
try:
unit = BYTE_UNIT_SCALE_MAP[scale]
factor = 1024**scale
except ValueError:
unit = "TiB"
factor = 1024**4
if factor > 1:
return "{:.1f}{}".format(n / factor, unit)
return "{}{}".format(n, unit)
def init_templating(app: quart.Quart) -> None: def init_templating(app: quart.Quart) -> None:
app.template_filter("repr")(repr) app.template_filter("repr")(repr)
app.template_filter("format_datetime")(flask_babel.format_datetime) app.template_filter("format_datetime")(flask_babel.format_datetime)
app.template_filter("format_date")(flask_babel.format_date) app.template_filter("format_date")(flask_babel.format_date)
app.template_filter("format_time")(flask_babel.format_time) app.template_filter("format_time")(flask_babel.format_time)
app.template_filter("format_timedelta")(flask_babel.format_timedelta) app.template_filter("format_timedelta")(flask_babel.format_timedelta)
app.template_filter("format_percent")(flask_babel.format_percent)
app.template_filter("format_bytes")(format_bytes)
app.template_filter("flatten")(flatten) app.template_filter("flatten")(flatten)
app.template_filter("circle_name")(circle_name) app.template_filter("circle_name")(circle_name)

View File

@@ -1175,3 +1175,41 @@ class ProsodyClient:
json=payload) as resp: json=payload) as resp:
resp.raise_for_status() resp.raise_for_status()
return (await resp.json())["jid"] return (await resp.json())["jid"]
@autosession
async def get_system_metrics(
self,
*,
session: aiohttp.ClientSession) -> typing.Mapping:
async with session.get(
self._admin_v1_endpoint("/server/metrics"),
) as resp:
if resp.status == 404:
return {}
self._raise_error_from_response(resp)
resp.raise_for_status()
return await resp.json()
@autosession
async def post_announcement(
self,
body: str,
recipients: str,
*,
session: aiohttp.ClientSession) -> None:
recipients_payload: typing.Union[str, typing.Sequence[str]]
if recipients == "self":
recipients_payload = [self.session_address]
else:
recipients_payload = recipients
payload = {
"recipients": recipients_payload,
"body": body,
}
async with session.post(
self._admin_v1_endpoint("/server/announcement"),
json=payload) as resp:
self._raise_error_from_response(resp)
resp.raise_for_status()

View File

@@ -52,6 +52,12 @@ licensed under the terms of the Apache 2.0 License -->
<path d="M0 0h24v24H0V0z" fill="none" /> <path d="M0 0h24v24H0V0z" fill="none" />
<path d="M12.65 10C11.7 7.31 8.9 5.5 5.77 6.12c-2.29.46-4.15 2.29-4.63 4.58C.32 14.57 3.26 18 7 18c2.61 0 4.83-1.67 5.65-4H17v2c0 1.1.9 2 2 2s2-.9 2-2v-2c1.1 0 2-.9 2-2s-.9-2-2-2h-8.35zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" /> <path d="M12.65 10C11.7 7.31 8.9 5.5 5.77 6.12c-2.29.46-4.15 2.29-4.63 4.58C.32 14.57 3.26 18 7 18c2.61 0 4.83-1.67 5.65-4H17v2c0 1.1.9 2 2 2s2-.9 2-2v-2c1.1 0 2-.9 2-2s-.9-2-2-2h-8.35zM7 14c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z" />
</symbol> </symbol>
<!-- from: communication/rss_feed/materialiconsround/24px.svg -->
<symbol id="icon-broadcast" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<circle cx="6.18" cy="17.82" r="2.18" />
<path d="M5.59 10.23c-.84-.14-1.59.55-1.59 1.4 0 .71.53 1.28 1.23 1.4 2.92.51 5.22 2.82 5.74 5.74.12.7.69 1.23 1.4 1.23.85 0 1.54-.75 1.41-1.59-.68-4.2-3.99-7.51-8.19-8.18zm-.03-5.71C4.73 4.43 4 5.1 4 5.93c0 .73.55 1.33 1.27 1.4 6.01.6 10.79 5.38 11.39 11.39.07.73.67 1.28 1.4 1.28.84 0 1.5-.73 1.42-1.56-.73-7.34-6.57-13.19-13.92-13.92z" />
</symbol>
<!-- from: content/add_circle_outline/materialiconsround/24px.svg --> <!-- from: content/add_circle_outline/materialiconsround/24px.svg -->
<symbol id="icon-add" viewBox="0 0 24 24"> <symbol id="icon-add" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" /> <path d="M0 0h24v24H0V0z" fill="none" />
@@ -77,6 +83,11 @@ licensed under the terms of the Apache 2.0 License -->
<path d="M0 0h24v24H0V0z" fill="none" /> <path d="M0 0h24v24H0V0z" fill="none" />
<path d="M21.94 11.23C21.57 8.76 19.32 7 16.82 7h-2.87c-.52 0-.95.43-.95.95s.43.95.95.95h2.9c1.6 0 3.04 1.14 3.22 2.73.17 1.43-.64 2.69-1.85 3.22l1.4 1.4c1.63-1.02 2.64-2.91 2.32-5.02zM4.12 3.56c-.39-.39-1.02-.39-1.41 0s-.39 1.02 0 1.41l2.4 2.4c-1.94.8-3.27 2.77-3.09 5.04C2.23 15.05 4.59 17 7.23 17h2.82c.52 0 .95-.43.95-.95s-.43-.95-.95-.95H7.16c-1.63 0-3.1-1.19-3.25-2.82-.15-1.72 1.11-3.17 2.75-3.35l2.1 2.1c-.43.09-.76.46-.76.92v.1c0 .52.43.95.95.95h1.78L13 15.27V17h1.73l3.3 3.3c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L4.12 3.56zM16 11.95c0-.52-.43-.95-.95-.95h-.66l1.49 1.49c.07-.13.12-.28.12-.44v-.1z" /> <path d="M21.94 11.23C21.57 8.76 19.32 7 16.82 7h-2.87c-.52 0-.95.43-.95.95s.43.95.95.95h2.9c1.6 0 3.04 1.14 3.22 2.73.17 1.43-.64 2.69-1.85 3.22l1.4 1.4c1.63-1.02 2.64-2.91 2.32-5.02zM4.12 3.56c-.39-.39-1.02-.39-1.41 0s-.39 1.02 0 1.41l2.4 2.4c-1.94.8-3.27 2.77-3.09 5.04C2.23 15.05 4.59 17 7.23 17h2.82c.52 0 .95-.43.95-.95s-.43-.95-.95-.95H7.16c-1.63 0-3.1-1.19-3.25-2.82-.15-1.72 1.11-3.17 2.75-3.35l2.1 2.1c-.43.09-.76.46-.76.92v.1c0 .52.43.95.95.95h1.78L13 15.27V17h1.73l3.3 3.3c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L4.12 3.56zM16 11.95c0-.52-.43-.95-.95-.95h-.66l1.49 1.49c.07-.13.12-.28.12-.44v-.1z" />
</symbol> </symbol>
<!-- from: content/send/materialiconsround/24px.svg -->
<symbol id="icon-send" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M3.4 20.4l17.45-7.48c.81-.35.81-1.49 0-1.84L3.4 3.6c-.66-.29-1.39.2-1.39.91L2 9.12c0 .5.37.93.87.99L17 12 2.87 13.88c-.5.07-.87.5-.87 1l.01 4.61c0 .71.73 1.2 1.39.91z" />
</symbol>
<!-- from: navigation/arrow_back/materialiconsround/24px.svg --> <!-- from: navigation/arrow_back/materialiconsround/24px.svg -->
<symbol id="icon-back" viewBox="0 0 24 24"> <symbol id="icon-back" viewBox="0 0 24 24">
<path d="M0 0h24v24H0V0z" fill="none" /> <path d="M0 0h24v24H0V0z" fill="none" />
@@ -142,4 +153,9 @@ licensed under the terms of the Apache 2.0 License -->
<path d="M0 0h24v24H0V0z" fill="none" /> <path d="M0 0h24v24H0V0z" fill="none" />
<path d="M17 7h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c1.65 0 3 1.35 3 3s-1.35 3-3 3h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-9 5c0 .55.45 1 1 1h6c.55 0 1-.45 1-1s-.45-1-1-1H9c-.55 0-1 .45-1 1zm2 3H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h3c.55 0 1-.45 1-1s-.45-1-1-1H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h3c.55 0 1-.45 1-1s-.45-1-1-1z" /> <path d="M17 7h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c1.65 0 3 1.35 3 3s-1.35 3-3 3h-3c-.55 0-1 .45-1 1s.45 1 1 1h3c2.76 0 5-2.24 5-5s-2.24-5-5-5zm-9 5c0 .55.45 1 1 1h6c.55 0 1-.45 1-1s-.45-1-1-1H9c-.55 0-1 .45-1 1zm2 3H7c-1.65 0-3-1.35-3-3s1.35-3 3-3h3c.55 0 1-.45 1-1s-.45-1-1-1H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h3c.55 0 1-.45 1-1s-.45-1-1-1z" />
</symbol> </symbol>
<!-- from: content/insights/materialiconsround/24px.svg -->
<symbol id="icon-insights" viewBox="0 0 24 24">
<g><rect fill="none" height="24" width="24" /><rect fill="none" height="24" width="24" /></g>
<g><g><path d="M21,8c-1.45,0-2.26,1.44-1.93,2.51l-3.55,3.56c-0.3-0.09-0.74-0.09-1.04,0l-2.55-2.55C12.27,10.45,11.46,9,10,9 c-1.45,0-2.27,1.44-1.93,2.52l-4.56,4.55C2.44,15.74,1,16.55,1,18c0,1.1,0.9,2,2,2c1.45,0,2.26-1.44,1.93-2.51l4.55-4.56 c0.3,0.09,0.74,0.09,1.04,0l2.55,2.55C12.73,16.55,13.54,18,15,18c1.45,0,2.27-1.44,1.93-2.52l3.56-3.55 C21.56,12.26,23,11.45,23,10C23,8.9,22.1,8,21,8z" /><polygon points="15,9 15.94,6.93 18,6 15.94,5.07 15,3 14.08,5.07 12,6 14.08,6.93" /><polygon points="3.5,11 4,9 6,8.5 4,8 3.5,6 3,8 1,8.5 3,9" /></g></g>
</symbol>
</defs></svg> </defs></svg>

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -31,6 +31,18 @@
<div>{% call standard_button("link", url_for(".invitations"), class="primary") %}{% trans %}Manage invitations{% endtrans %}{% endcall %}</div> <div>{% call standard_button("link", url_for(".invitations"), class="primary") %}{% trans %}Manage invitations{% endtrans %}{% endcall %}</div>
{#- -#} {#- -#}
</li> </li>
<li>
<h2>{% trans %}System health{% endtrans %}</h2>
{#- -#}
{%- if show_metrics -%}
<p>{% trans %}View the server status or send a broadcast message to all users.{% endtrans %}</p>
{%- else -%}
<p>{% trans %}Send a broadcast message to all users.{% endtrans %}</p>
{%- endif -%}
{#- -#}
<div>{% call standard_button("insights", url_for(".system"), class="primary") %}{% trans %}Manage system{% endtrans %}{% endcall %}</div>
{#- -#}
</li>
<li> <li>
{#- -#} {#- -#}
<p>{% trans %}Go back to your user's web portal page.{% endtrans %}</p> <p>{% trans %}Go back to your user's web portal page.{% endtrans %}</p>

View File

@@ -0,0 +1,97 @@
{% extends "admin_app.html" %}
{% from "library.j2" import form_button %}
{% block content %}
<h1>{% trans %}Manage system{% endtrans %}</h1>
{% if show_metrics %}
<h2>{% trans %}Overall system status{% endtrans %}</h2>
<div class="elevated el-2">
<dl>
<dt>{% trans %}System load (5 minute average){% endtrans %}</dt>
<dd>
{%- if metrics.load5 -%}
{{ metrics.load5 }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
<dt>{% trans %}Memory use{% endtrans %}</dt>
<dd>
{%- if metrics.mem_total and metrics.mem_available -%}
{% trans percentage_global=((1 - (metrics.mem_available / metrics.mem_total)) | format_percent), percentage_snikket=((((metrics.prosody_rss | default(0)) + (metrics.portal_rss | default(0))) / metrics.mem_total) | format_percent), mem_available=(metrics.mem_total | format_bytes) %}{{ percentage_global }} of {{ mem_available }}. Of that, Snikket uses {{ percentage_snikket }}.{% endtrans %}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
</dl>
</div>
<h2>{% trans %}Web portal status{% endtrans %}</h2>
<div class="elevated el-2">
<dl>
<dt>{% trans %}Version{% endtrans %}</dt>
<dd>{{ version }} <a href="{{ url_for("main.about") }}">{% trans %}View all versions{% endtrans %}</a></dd>
<dt>{% trans %}Average CPU use{% endtrans %}</dt>
<dd>
{%- if metrics.portal_cpu -%}
{{ metrics.portal_cpu | format_percent }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
<dt>{% trans %}Current memory use{% endtrans %}</dt>
<dd>
{%- if metrics.portal_rss -%}
{{ metrics.portal_rss | format_bytes }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
</dl>
</div>
<h2>{% trans %}Snikket server status{% endtrans %}</h2>
<div class="elevated el-2">
<dl>
<dt>{% trans %}Version{% endtrans %}</dt>
<dd>{{ prosody_version }} <a href="{{ url_for("main.about") }}">{% trans %}View all versions{% endtrans %}</a></dd>
<dt>{% trans %}Average CPU use{% endtrans %}</dt>
<dd>
{%- if metrics.prosody_cpu -%}
{{ metrics.prosody_cpu | format_percent }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
<dt>{% trans %}Current memory use{% endtrans %}</dt>
<dd>
{%- if metrics.prosody_rss -%}
{{ metrics.prosody_rss | format_bytes }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
<dt>{% trans %}Connected devices{% endtrans %}</dt>
<dd>
{%- if metrics.prosody_devices | default(None) is not none -%}
{{ metrics.prosody_devices }}
{%- else -%}
<em>{% trans %}unknown{% endtrans %}</em>
{%- endif -%}
</dd>
</dl>
</div>
{% endif %}
<h2>{% trans %}Broadcast message{% endtrans %}</h2>
<form method="POST">{{ form.csrf_token }}<div class="form layout-expanded">
<p class="form-desc">{% trans %}This form allows you to send a message to all users currently online on your Snikket server. Use it wisely.{% endtrans %}</p>
<div class="f-ebox">
{{ form.text.label }}
{{ form.text }}
</div>
<div class="f-ebox">
{{ form.online_only }}{{ form.online_only.label }}
</div>
<div class="f-bbox">
{%- call form_button("send", form.action_send_preview, class="primary") -%}{%- endcall -%}
{%- call form_button("broadcast", form.action_post_all, class="secondary accent") -%}{%- endcall -%}
</div>
</div></form>
{% endblock %}

View File

@@ -8,186 +8,206 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2021-03-25 17:32+0100\n" "POT-Creation-Date: 2021-05-27 17:59+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.9.0\n" "Generated-By: Babel 2.9.1\n"
#: snikket_web/admin.py:59 #: snikket_web/admin.py:66
msgid "Limited" msgid "Limited"
msgstr "" msgstr ""
#: snikket_web/admin.py:64 snikket_web/templates/admin_delete_user.html:10 #: snikket_web/admin.py:71 snikket_web/templates/admin_delete_user.html:10
#: snikket_web/templates/admin_users.html:8 #: snikket_web/templates/admin_users.html:8
msgid "Login name" msgid "Login name"
msgstr "" msgstr ""
#: snikket_web/admin.py:68 snikket_web/templates/admin_delete_user.html:12 #: snikket_web/admin.py:75 snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:61 #: snikket_web/templates/admin_users.html:9 snikket_web/user.py:61
msgid "Display name" msgid "Display name"
msgstr "" msgstr ""
#: snikket_web/admin.py:72 snikket_web/templates/admin_edit_user.html:33 #: snikket_web/admin.py:79 snikket_web/templates/admin_edit_user.html:32
msgid "Access Level" msgid "Access Level"
msgstr "" msgstr ""
#: snikket_web/admin.py:77 #: snikket_web/admin.py:84
msgid "Normal user" msgid "Normal user"
msgstr "" msgstr ""
#: snikket_web/admin.py:78 #: snikket_web/admin.py:85
msgid "Administrator" msgid "Administrator"
msgstr "" msgstr ""
#: snikket_web/admin.py:83 #: snikket_web/admin.py:90
msgid "Update user" msgid "Update user"
msgstr "" msgstr ""
#: snikket_web/admin.py:87 #: snikket_web/admin.py:94
msgid "Create password reset link" msgid "Create password reset link"
msgstr "" msgstr ""
#: snikket_web/admin.py:105 #: snikket_web/admin.py:112
msgid "Password reset link created" msgid "Password reset link created"
msgstr "" msgstr ""
#: snikket_web/admin.py:120 #: snikket_web/admin.py:127
msgid "User information updated." msgid "User information updated."
msgstr "" msgstr ""
#: snikket_web/admin.py:142 #: snikket_web/admin.py:149
msgid "Delete user permanently" msgid "Delete user permanently"
msgstr "" msgstr ""
#: snikket_web/admin.py:155 #: snikket_web/admin.py:162
msgid "User deleted" msgid "User deleted"
msgstr "" msgstr ""
#: snikket_web/admin.py:193 #: snikket_web/admin.py:200
msgid "Password reset link not found" msgid "Password reset link not found"
msgstr "" msgstr ""
#: snikket_web/admin.py:205 #: snikket_web/admin.py:212
msgid "Password reset link deleted" msgid "Password reset link deleted"
msgstr "" msgstr ""
#: snikket_web/admin.py:225 #: snikket_web/admin.py:232
msgid "Invite to circle" msgid "Invite to circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:231 #: snikket_web/admin.py:238
msgid "At least one circle must be selected" msgid "At least one circle must be selected"
msgstr "" msgstr ""
#: snikket_web/admin.py:236 #: snikket_web/admin.py:243
msgid "Valid for" msgid "Valid for"
msgstr "" msgstr ""
#: snikket_web/admin.py:238 #: snikket_web/admin.py:245
msgid "One hour" msgid "One hour"
msgstr "" msgstr ""
#: snikket_web/admin.py:239 #: snikket_web/admin.py:246
msgid "Twelve hours" msgid "Twelve hours"
msgstr "" msgstr ""
#: snikket_web/admin.py:240 #: snikket_web/admin.py:247
msgid "One day" msgid "One day"
msgstr "" msgstr ""
#: snikket_web/admin.py:241 #: snikket_web/admin.py:248
msgid "One week" msgid "One week"
msgstr "" msgstr ""
#: snikket_web/admin.py:242 #: snikket_web/admin.py:249
msgid "Four weeks" msgid "Four weeks"
msgstr "" msgstr ""
#: snikket_web/admin.py:248 snikket_web/templates/admin_edit_invite.html:17 #: snikket_web/admin.py:255 snikket_web/templates/admin_edit_invite.html:17
msgid "Invitation type" msgid "Invitation type"
msgstr "" msgstr ""
#: snikket_web/admin.py:250 snikket_web/templates/library.j2:116 #: snikket_web/admin.py:257 snikket_web/templates/library.j2:116
msgid "Individual" msgid "Individual"
msgstr "" msgstr ""
#: snikket_web/admin.py:251 snikket_web/templates/library.j2:114 #: snikket_web/admin.py:258 snikket_web/templates/library.j2:114
msgid "Group" msgid "Group"
msgstr "" msgstr ""
#: snikket_web/admin.py:257 #: snikket_web/admin.py:264
msgid "New invitation link" msgid "New invitation link"
msgstr "" msgstr ""
#: snikket_web/admin.py:319 #: snikket_web/admin.py:326
msgid "Revoke" msgid "Revoke"
msgstr "" msgstr ""
#: snikket_web/admin.py:343 #: snikket_web/admin.py:350
msgid "Invitation created" msgid "Invitation created"
msgstr "" msgstr ""
#: snikket_web/admin.py:359 #: snikket_web/admin.py:366
msgid "No such invitation exists" msgid "No such invitation exists"
msgstr "" msgstr ""
#: snikket_web/admin.py:374 #: snikket_web/admin.py:381
msgid "Invitation revoked" msgid "Invitation revoked"
msgstr "" msgstr ""
#: snikket_web/admin.py:391 snikket_web/admin.py:439 #: snikket_web/admin.py:398 snikket_web/admin.py:446
msgid "Name" msgid "Name"
msgstr "" msgstr ""
#: snikket_web/admin.py:396 snikket_web/templates/admin_circles.html:47 #: snikket_web/admin.py:403 snikket_web/templates/admin_circles.html:47
msgid "Create circle" msgid "Create circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:426 #: snikket_web/admin.py:433
msgid "Circle created" msgid "Circle created"
msgstr "" msgstr ""
#: snikket_web/admin.py:444 #: snikket_web/admin.py:451
msgid "Select user" msgid "Select user"
msgstr "" msgstr ""
#: snikket_web/admin.py:449 #: snikket_web/admin.py:456
msgid "Update circle" msgid "Update circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:453 #: snikket_web/admin.py:460
msgid "Delete circle permanently" msgid "Delete circle permanently"
msgstr "" msgstr ""
#: snikket_web/admin.py:459 #: snikket_web/admin.py:466
msgid "Add user" msgid "Add user"
msgstr "" msgstr ""
#: snikket_web/admin.py:475 #: snikket_web/admin.py:482
msgid "No such circle exists" msgid "No such circle exists"
msgstr "" msgstr ""
#: snikket_web/admin.py:512 #: snikket_web/admin.py:519
msgid "Circle data updated" msgid "Circle data updated"
msgstr "" msgstr ""
#: snikket_web/admin.py:518 #: snikket_web/admin.py:525
msgid "Circle deleted" msgid "Circle deleted"
msgstr "" msgstr ""
#: snikket_web/admin.py:529 #: snikket_web/admin.py:536
msgid "User added to circle" msgid "User added to circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:538 #: snikket_web/admin.py:545
msgid "User removed from circle" msgid "User removed from circle"
msgstr "" msgstr ""
#: snikket_web/infra.py:41 #: snikket_web/admin.py:616
msgid "Message contents"
msgstr ""
#: snikket_web/admin.py:622
msgid "Only send to online users"
msgstr ""
#: snikket_web/admin.py:626
msgid "Post to all users"
msgstr ""
#: snikket_web/admin.py:630
msgid "Send preview to yourself"
msgstr ""
#: snikket_web/admin.py:652
msgid "Announcement sent!"
msgstr ""
#: snikket_web/infra.py:51
msgid "Main" msgid "Main"
msgstr "" msgstr ""
@@ -497,7 +517,7 @@ msgid "Delete user %(user_name)s"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_user.html:6 #: snikket_web/templates/admin_delete_user.html:6
#: snikket_web/templates/admin_edit_user.html:54 #: snikket_web/templates/admin_edit_user.html:53
msgid "Delete user" msgid "Delete user"
msgstr "" msgstr ""
@@ -664,56 +684,56 @@ msgstr ""
msgid "Edit user %(user_name)s" msgid "Edit user %(user_name)s"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:23 #: snikket_web/templates/admin_edit_user.html:22
msgid "Edit user" msgid "Edit user"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:27 #: snikket_web/templates/admin_edit_user.html:26
msgid "The login name cannot be changed." msgid "The login name cannot be changed."
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:34 #: snikket_web/templates/admin_edit_user.html:33
msgid "" msgid ""
"The access level of a user determines what interactions are allowed for " "The access level of a user determines what interactions are allowed for "
"them on your Snikket service." "them on your Snikket service."
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:41 #: snikket_web/templates/admin_edit_user.html:40
#, python-format #, python-format
msgid "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>" msgid "<strong>%(title)s%(icon)s</strong><p>%(description)s</p>"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:51 #: snikket_web/templates/admin_edit_user.html:50
msgid "Return to user list" msgid "Return to user list"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:59 #: snikket_web/templates/admin_edit_user.html:58
msgid "Further actions" msgid "Further actions"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:61 #: snikket_web/templates/admin_edit_user.html:60
msgid "Reset password" msgid "Reset password"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:64 #: snikket_web/templates/admin_edit_user.html:63
msgid "" msgid ""
"If the user has lost their password, you can use the button below to " "If the user has lost their password, you can use the button below to "
"create a special link which allows to change the password of the account," "create a special link which allows to change the password of the account,"
" once." " once."
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:69 #: snikket_web/templates/admin_edit_user.html:68
msgid "Debug information" msgid "Debug information"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:71 #: snikket_web/templates/admin_edit_user.html:70
msgid "" msgid ""
"In some cases, extended information about the user account and the " "In some cases, extended information about the user account and the "
"connected devices is necessary to troubleshoot issues. The button below " "connected devices is necessary to troubleshoot issues. The button below "
"reveals this (sensitive) information." "reveals this (sensitive) information."
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_user.html:75 #: snikket_web/templates/admin_edit_user.html:74
msgid "Show debug information" msgid "Show debug information"
msgstr "" msgstr ""
@@ -756,11 +776,28 @@ msgstr ""
msgid "Manage invitations" msgid "Manage invitations"
msgstr "" msgstr ""
#: snikket_web/templates/admin_home.html:36 #: snikket_web/templates/admin_home.html:35
msgid "Go back to your user's web portal page." msgid "System health"
msgstr "" msgstr ""
#: snikket_web/templates/admin_home.html:38 #: snikket_web/templates/admin_home.html:38
msgid "View the server status or send a broadcast message to all users."
msgstr ""
#: snikket_web/templates/admin_home.html:40
msgid "Send a broadcast message to all users."
msgstr ""
#: snikket_web/templates/admin_home.html:43
#: snikket_web/templates/admin_system.html:4
msgid "Manage system"
msgstr ""
#: snikket_web/templates/admin_home.html:48
msgid "Go back to your user's web portal page."
msgstr ""
#: snikket_web/templates/admin_home.html:50
msgid "Exit admin panel" msgid "Exit admin panel"
msgstr "" msgstr ""
@@ -811,6 +848,77 @@ msgstr ""
msgid "Destroy link" msgid "Destroy link"
msgstr "" msgstr ""
#: snikket_web/templates/admin_system.html:6
msgid "Overall system status"
msgstr ""
#: snikket_web/templates/admin_system.html:9
msgid "System load (5 minute average)"
msgstr ""
#: snikket_web/templates/admin_system.html:14
#: snikket_web/templates/admin_system.html:22
#: snikket_web/templates/admin_system.html:37
#: snikket_web/templates/admin_system.html:45
#: snikket_web/templates/admin_system.html:60
#: snikket_web/templates/admin_system.html:68
#: snikket_web/templates/admin_system.html:76
msgid "unknown"
msgstr ""
#: snikket_web/templates/admin_system.html:17
msgid "Memory use"
msgstr ""
#: snikket_web/templates/admin_system.html:20
#, python-format
msgid ""
"%(percentage_global)s of %(mem_available)s. Of that, Snikket uses "
"%(percentage_snikket)s."
msgstr ""
#: snikket_web/templates/admin_system.html:27
msgid "Web portal status"
msgstr ""
#: snikket_web/templates/admin_system.html:30
#: snikket_web/templates/admin_system.html:53
msgid "Version"
msgstr ""
#: snikket_web/templates/admin_system.html:31
#: snikket_web/templates/admin_system.html:54
msgid "View all versions"
msgstr ""
#: snikket_web/templates/admin_system.html:32
#: snikket_web/templates/admin_system.html:55
msgid "Average CPU use"
msgstr ""
#: snikket_web/templates/admin_system.html:40
#: snikket_web/templates/admin_system.html:63
msgid "Current memory use"
msgstr ""
#: snikket_web/templates/admin_system.html:50
msgid "Snikket server status"
msgstr ""
#: snikket_web/templates/admin_system.html:71
msgid "Connected devices"
msgstr ""
#: snikket_web/templates/admin_system.html:82
msgid "Broadcast message"
msgstr ""
#: snikket_web/templates/admin_system.html:84
msgid ""
"This form allows you to send a message to all users currently online on "
"your Snikket server. Use it wisely."
msgstr ""
#: snikket_web/templates/admin_users.html:19 #: snikket_web/templates/admin_users.html:19
msgid "The user is an administrator." msgid "The user is an administrator."
msgstr "" msgstr ""

View File

@@ -8,11 +8,13 @@ action/exit_to_app:exit_to_app
action/lock:lock action/lock:lock
communication/qr_code:qrcode communication/qr_code:qrcode
communication/vpn_key:passwd communication/vpn_key:passwd
communication/rss_feed:broadcast
content/add_circle_outline:add content/add_circle_outline:add
content/add_link:create_link content/add_link:create_link
content/remove_circle_outline:remove content/remove_circle_outline:remove
content/content_copy:copy content/content_copy:copy
content/link_off:remove_link content/link_off:remove_link
content/send:send
navigation/arrow_back:back navigation/arrow_back:back
navigation/arrow_forward:forward navigation/arrow_forward:forward
navigation/cancel:cancel navigation/cancel:cancel
@@ -26,3 +28,4 @@ navigation/close:close
image/edit:edit image/edit:edit
action/admin_panel_settings:admin action/admin_panel_settings:admin
content/link:link content/link:link
content/insights:insights