This commit is contained in:
ab committed 2023-09-25 01:19:50 +03:00
commit 39e480b655
15 files changed
+786

No files matched your search

+1
View File
@@ -0,0 +1 @@
config.yaml
+3
View File
@@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+6
View File
@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (OutlineFleet)" project-jdk-type="Python SDK" />
<component name="PyCharmProfessionalAdvertiser">
<option name="shown" value="true" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/OutlineFleet.iml" filepath="$PROJECT_DIR$/.idea/OutlineFleet.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
+70
View File
@@ -0,0 +1,70 @@
import logging
from typing import TypedDict, List
from outline_vpn.outline_vpn import OutlineKey, OutlineVPN
import yaml
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%d-%m-%Y %H:%M:%S')
log = logging.getLogger('OutlineFleet.lib')
class ServerDict(TypedDict):
name: str
url: str
cert: str
comment: str
server_id: str
metrics_enabled: str
created_timestamp_ms: int
version: str
port_for_new_access_keys: int
hostname_for_access_keys: str
keys: List[OutlineKey]
class Server:
def __init__(self,
url: str,
cert: str,
comment: str,
):
self.client = OutlineVPN(api_url=url, cert_sha256=cert)
self.data: ServerDict = {
'name': self.client.get_server_information()["name"],
'url': url,
'cert': cert,
'comment': comment,
'server_id': self.client.get_server_information()["serverId"],
'metrics_enabled': self.client.get_server_information()["metricsEnabled"],
'created_timestamp_ms': self.client.get_server_information()["createdTimestampMs"],
'version': self.client.get_server_information()["version"],
'port_for_new_access_keys': self.client.get_server_information()["portForNewAccessKeys"],
'hostname_for_access_keys': self.client.get_server_information()["hostnameForAccessKeys"],
'keys': self.client.get_keys()
}
def info(self) -> ServerDict:
return self.data
def apply_config(self, config):
if config.get("name"):
self.client.set_server_name(config.get("name"))
log.info("Changed %s name to '%s'", self.data["server_id"], config.get("name"))
if config.get("metrics"):
self.client.set_metrics_status(True if config.get("metrics") == 'True' else False)
log.info("Changed %s metrics status to '%s'", self.data["server_id"], config.get("metrics"))
if config.get("port_for_new_access_keys"):
self.client.set_port_new_for_access_keys(int(config.get("port_for_new_access_keys")))
log.info("Changed %s port_for_new_access_keys to '%s'", self.data["server_id"], config.get("port_for_new_access_keys"))
if config.get("hostname_for_access_keys"):
self.client.set_hostname(config.get("hostname_for_access_keys"))
log.info("Changed %s hostname_for_access_keys to '%s'", self.data["server_id"], config.get("hostname_for_access_keys"))
if config.get("comment"):
with open("config.yaml", "r") as file:
config_file = yaml.safe_load(file) or {}
config_file["servers"][self.data['server_id']]['comment'] = config.get("comment")
with open("config.yaml", "w") as file:
yaml.safe_dump(config_file, file)
log.info("Changed %s comment to '%s'", self.data["server_id"], config.get("comment"))
+91
View File
@@ -0,0 +1,91 @@
from outline_vpn.outline_vpn import OutlineVPN
import yaml
import logging
from datetime import datetime
from flask import Flask, render_template, request, url_for, redirect
from lib import Server
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%d-%m-%Y %H:%M:%S')
log = logging.getLogger('OutlineFleet')
SERVERS = list()
app = Flask(__name__)
def format_timestamp(ts):
return datetime.fromtimestamp(ts//1000).strftime('%Y-%m-%d %H:%M:%S')
def update_state():
global SERVERS
SERVERS = list()
config = dict()
try:
with open("config.yaml", "r") as file:
config = yaml.safe_load(file)
except:
with open("config.yaml", "w"):
pass
if config:
servers = config.get('servers', None)
for server_id, config in servers.items():
server = Server(url=config["url"], cert=config["cert"], comment=config["comment"])
SERVERS.append(server)
log.info("Server found: %s", server.info()["name"])
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
return render_template(
'index.html',
SERVERS=SERVERS,
nt=request.args.get('nt'),
nl=request.args.get('nl'),
selected_server=request.args.get('selected_server'),
format_timestamp=format_timestamp)
else:
server = request.form['server_id']
server = next((item for item in SERVERS if item.info()["server_id"] == server), None)
server.apply_config(request.form)
update_state()
return redirect(url_for('index', nt="Updated Outline VPN Server", selected_server=request.args.get('selected_server')))
@app.route('/add_server', methods=['GET', 'POST'])
def add_server():
if request.method == 'GET':
return render_template('add_server.html')
else:
with open("config.yaml", "r") as file:
config = yaml.safe_load(file) or {}
servers = config.get('servers', dict())
try:
new_server = Server(url=request.form['url'], cert=request.form['cert'], comment=request.form['comment'])
except:
return redirect(url_for('index', nt="Couldn't access Outline VPN Server", nl="error"))
servers[new_server.data["server_id"]] = {
'name': new_server.data["name"],
'url': new_server.data["url"],
'comment': new_server.data["comment"],
'cert': request.form['cert']
}
config["servers"] = servers
with open("config.yaml", "w") as file:
yaml.safe_dump(config, file)
update_state()
return redirect(url_for('index', nt="Added Outline VPN Server"))
if __name__ == '__main__':
update_state()
app.run()
+3
View File
@@ -0,0 +1,3 @@
outline-vpn-api
PyYAML
Flask
+355
View File
@@ -0,0 +1,355 @@
/*
* -- BASE STYLES --
* Most of these are inherited from Base, but I want to change a few.
*/
body {
color: #333;
}
a {
text-decoration: none;
color: #1b98f8;
}
/*
* -- HELPER STYLES --
* Over-riding some of the .pure-button styles to make my buttons look unique
*/
.primary-button,
.secondary-button {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
border-radius: 20px;
}
.primary-button {
color: #fff;
background: #1b98f8;
margin: 1em 0;
}
.secondary-button {
background: #fff;
border: 1px solid #ddd;
color: #666;
padding: 0.5em 2em;
font-size: 80%;
}
/*
* -- LAYOUT STYLES --
* This layout consists of three main elements, `#nav` (navigation bar), `#list` (server list), and `#main` (server content). All 3 elements are within `#layout`
*/
#layout, #nav, #list, #main {
margin: 0;
padding: 0;
}
/* Make the navigation 100% width on phones */
#nav {
width: 100%;
height: 40px;
position: relative;
background: rgb(37, 42, 58);
text-align: center;
}
/* Show the "Menu" button on phones */
#nav .nav-menu-button {
display: block;
top: 0.5em;
right: 0.5em;
position: absolute;
}
/* When "Menu" is clicked, the navbar should be 80% height */
#nav.active {
height: 80%;
}
/* Don't show the navigation items... */
.nav-inner {
display: none;
}
/* ...until the "Menu" button is clicked */
#nav.active .nav-inner {
display: block;
padding: 2em 0;
}
/*
* -- NAV BAR STYLES --
* Styling the default .pure-menu to look a little more unique.
*/
#nav .pure-menu {
background: transparent;
border: none;
text-align: left;
}
#nav .pure-menu-link:hover,
#nav .pure-menu-link:focus {
background: rgb(55, 60, 90);
}
#nav .pure-menu-link {
color: #fff;
margin-left: 0.5em;
}
#nav .pure-menu-heading {
border-bottom: none;
font-size:110%;
color: rgb(75, 113, 151);
}
/*
* -- server STYLES --
* Styles relevant to the server messages, labels, counts, and more.
*/
.server-count {
color: rgb(75, 113, 151);
}
.server-label-personal,
.server-label-work,
.server-label-travel {
width: 15px;
height: 15px;
display: inline-block;
margin-right: 0.5em;
border-radius: 3px;
}
.server-label-personal {
background: #ffc94c;
}
.server-label-work {
background: #41ccb4;
}
.server-label-travel {
background: #40c365;
}
/* server Item Styles */
.server-item {
padding: 0.9em 1em;
border-bottom: 1px solid #ddd;
border-left: 6px solid transparent;
}
.server-name {
text-transform: uppercase;
}
.server-name,
.server-info {
margin: 0;
font-size: 100%;
}
.server-info {
color: #999;
font-size: 80%;
}
.server-comment {
font-size: 90%;
margin: 0.4em 0;
}
.server-add {
cursor: pointer;
text-align: center;
font-size: 150%;
color: #999;
}
.server-add:hover {
color: black;
}
.server-item-selected {
background: #eee;
}
.server-item-unread {
border-left: 6px solid #1b98f8;
}
/* server Content Styles */
.server-content-header, .server-content-body, .server-content-footer {
padding: 1em 2em;
}
.server-content-header {
border-bottom: 1px solid #ddd;
}
.server-content-title {
margin: 0.5em 0 0;
}
.server-content-subtitle {
font-size: 1em;
margin: 0;
font-weight: normal;
}
.server-content-subtitle span {
color: #999;
}
.server-content-controls {
margin-top: 2em;
text-align: right;
}
.server-content-controls .secondary-button {
margin-bottom: 0.3em;
}
.server-avatar {
width: 40px;
height: 40px;
}
/*
* -- TABLET (AND UP) MEDIA QUERIES --
* On tablets and other medium-sized devices, we want to customize some
* of the mobile styles.
*/
@media (min-width: 40em) {
/* Move the layout over so we can fit the nav + list in on the left */
#layout {
padding-left:500px; /* "left col (nav + list)" width */
position: relative;
}
/* These are position:fixed; elements that will be in the left 500px of the screen */
#nav, #list {
position: fixed;
top: 0;
bottom: 0;
overflow: auto;
}
#nav {
margin-left:-500px; /* "left col (nav + list)" width */
width:150px;
height: 100%;
}
/* Show the menu items on the larger screen */
.nav-inner {
display: block;
padding: 2em 0;
}
/* Hide the "Menu" button on larger screens */
#nav .nav-menu-button {
display: none;
}
#list {
margin-left: -350px;
width: 100%;
height: 33%;
border-bottom: 1px solid #ddd;
}
#main {
position: fixed;
top: 33%;
right: 0;
bottom: 0;
left: 150px;
overflow: auto;
width: auto; /* so that it's not 100% */
}
}
/*
* -- DESKTOP (AND UP) MEDIA QUERIES --
* On desktops and other large-sized devices, we want to customize some
* of the mobile styles.
*/
@media (min-width: 60em) {
/* This will take up the entire height, and be a little thinner */
#list {
margin-left: -350px;
width:350px;
height: 100%;
border-right: 1px solid #ddd;
}
/* This will now take up it's own column, so don't need position: fixed; */
#main {
position: static;
margin: 0;
padding: 0;
}
}
.alert {
position: absolute;
top: 1em;
right: 1em;
width: auto;
height: auto;
padding: 10px;
margin: 10px;
line-height: 1.8;
border-radius: 5px;
cursor: hand;
cursor: pointer;
font-family: sans-serif;
font-weight: 400;
}
.alertCheckbox {
display: none;
}
:checked + .alert {
display: none;
}
.alertText {
display: table;
margin: 0 auto;
text-align: center;
font-size: 150%;
}
.alertClose {
float: right;
padding-top: 0px;
font-size: 120%;
}
.clear {
clear: both;
}
.info {
background-color: #EEE;
border: 1px solid #DDD;
color: #999;
}
.success {
background-color: #EFE;
border: 1px solid #DED;
color: #9A9;
}
.notice {
background-color: #EFF;
border: 1px solid #DEE;
color: #9AA;
}
.warning {
background-color: #FDF7DF;
border: 1px solid #FEEC6F;
color: #C9971C;
}
.error {
background-color: #FEE;
border: 1px solid #EDD;
color: #A66;
}
+11
View File
@@ -0,0 +1,11 @@
/*!
Pure v3.0.0
Copyright 2013 Yahoo!
Licensed under the BSD License.
https://github.com/pure-css/pure/blob/master/LICENSE
*/
/*!
normalize.css v | MIT License | https://necolas.github.io/normalize.css/
Copyright (c) Nicolas Gallagher and Jonathan Neal
*/
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}details{display:block}summary{display:list-item}template{display:none}[hidden]{display:none}html{font-family:sans-serif}.hidden,[hidden]{display:none!important}.pure-img{max-width:100%;height:auto;display:block}.pure-g{display:flex;flex-flow:row wrap;align-content:flex-start}.pure-u{display:inline-block;vertical-align:top}.pure-u-1,.pure-u-1-1,.pure-u-1-12,.pure-u-1-2,.pure-u-1-24,.pure-u-1-3,.pure-u-1-4,.pure-u-1-5,.pure-u-1-6,.pure-u-1-8,.pure-u-10-24,.pure-u-11-12,.pure-u-11-24,.pure-u-12-24,.pure-u-13-24,.pure-u-14-24,.pure-u-15-24,.pure-u-16-24,.pure-u-17-24,.pure-u-18-24,.pure-u-19-24,.pure-u-2-24,.pure-u-2-3,.pure-u-2-5,.pure-u-20-24,.pure-u-21-24,.pure-u-22-24,.pure-u-23-24,.pure-u-24-24,.pure-u-3-24,.pure-u-3-4,.pure-u-3-5,.pure-u-3-8,.pure-u-4-24,.pure-u-4-5,.pure-u-5-12,.pure-u-5-24,.pure-u-5-5,.pure-u-5-6,.pure-u-5-8,.pure-u-6-24,.pure-u-7-12,.pure-u-7-24,.pure-u-7-8,.pure-u-8-24,.pure-u-9-24{display:inline-block;letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-u-1-24{width:4.1667%}.pure-u-1-12,.pure-u-2-24{width:8.3333%}.pure-u-1-8,.pure-u-3-24{width:12.5%}.pure-u-1-6,.pure-u-4-24{width:16.6667%}.pure-u-1-5{width:20%}.pure-u-5-24{width:20.8333%}.pure-u-1-4,.pure-u-6-24{width:25%}.pure-u-7-24{width:29.1667%}.pure-u-1-3,.pure-u-8-24{width:33.3333%}.pure-u-3-8,.pure-u-9-24{width:37.5%}.pure-u-2-5{width:40%}.pure-u-10-24,.pure-u-5-12{width:41.6667%}.pure-u-11-24{width:45.8333%}.pure-u-1-2,.pure-u-12-24{width:50%}.pure-u-13-24{width:54.1667%}.pure-u-14-24,.pure-u-7-12{width:58.3333%}.pure-u-3-5{width:60%}.pure-u-15-24,.pure-u-5-8{width:62.5%}.pure-u-16-24,.pure-u-2-3{width:66.6667%}.pure-u-17-24{width:70.8333%}.pure-u-18-24,.pure-u-3-4{width:75%}.pure-u-19-24{width:79.1667%}.pure-u-4-5{width:80%}.pure-u-20-24,.pure-u-5-6{width:83.3333%}.pure-u-21-24,.pure-u-7-8{width:87.5%}.pure-u-11-12,.pure-u-22-24{width:91.6667%}.pure-u-23-24{width:95.8333%}.pure-u-1,.pure-u-1-1,.pure-u-24-24,.pure-u-5-5{width:100%}.pure-button{display:inline-block;line-height:normal;white-space:nowrap;vertical-align:middle;text-align:center;cursor:pointer;-webkit-user-drag:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.pure-button::-moz-focus-inner{padding:0;border:0}.pure-button-group{letter-spacing:-.31em;text-rendering:optimizespeed}.opera-only :-o-prefocus,.pure-button-group{word-spacing:-0.43em}.pure-button-group .pure-button{letter-spacing:normal;word-spacing:normal;vertical-align:top;text-rendering:auto}.pure-button{font-family:inherit;font-size:100%;padding:.5em 1em;color:rgba(0,0,0,.8);border:none transparent;background-color:#e6e6e6;text-decoration:none;border-radius:2px}.pure-button-hover,.pure-button:focus,.pure-button:hover{background-image:linear-gradient(transparent,rgba(0,0,0,.05) 40%,rgba(0,0,0,.1))}.pure-button:focus{outline:0}.pure-button-active,.pure-button:active{box-shadow:0 0 0 1px rgba(0,0,0,.15) inset,0 0 6px rgba(0,0,0,.2) inset;border-color:#000}.pure-button-disabled,.pure-button-disabled:active,.pure-button-disabled:focus,.pure-button-disabled:hover,.pure-button[disabled]{border:none;background-image:none;opacity:.4;cursor:not-allowed;box-shadow:none;pointer-events:none}.pure-button-hidden{display:none}.pure-buLine truncated
+20
View File
@@ -0,0 +1,20 @@
{% extends "base.html" %}
{% block title %}Add new server{% endblock %}
{% block content %}
<form class="pure-form pure-form-stacked" method="POST">
<fieldset>
<div class="pure-g">
<div class="pure-u-1 pure-u-md-1-3">
<input type="text" class="pure-u-23-24" name="url" placeholder="Server management URL"/>
</div>
<div class="pure-u-1 pure-u-md-1-3">
<input type="text"class="pure-u-23-24" name="cert" placeholder="Certificate"/>
</div>
<div class="pure-u-1 pure-u-md-1-3">
<input type="text" class="pure-u-23-24" name="comment" placeholder="Comment"/>
</div>
</div>
<button type="submit" class="pure-button pure-input-1 pure-button-primary">Add</button>
</fieldset>
</form>
{% endblock %}
+90
View File
@@ -0,0 +1,90 @@
<!DOCTYPE html>
<html>
<head>
{% block title %}Dashboard{% endblock %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='pure.css') }}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='layout.css') }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="layout" class="content pure-g">
<div id="nav" class="pure-u-1-3">
<a href="#" id="menuLink" class="nav-menu-button">Menu</a>
<div class="nav-inner">
<button onclick="location.href='/';" style="cursor:pointer;" class="primary-button pure-button">OutlineFleet</button>
<div class="pure-menu custom-restricted-width">
<ul class="pure-menu-list">
<li class="pure-menu-item"><a href="/" class="pure-menu-link">Servers</a></li>
<li class="pure-menu-item"><a href="#" class="pure-menu-link">Clients</a></li>
</ul>
</div>
</div>
</div>
{% block content %}{% endblock %}
</div>
<!-- Script to make the Menu link work -->
<!-- Just stripped down version of the js/ui.js script for the side-menu layout -->
<script>
function getElements() {
return {
menu: document.getElementById('nav'),
menuLink: document.getElementById('menuLink')
};
}
function toggleClass(element, className) {
var classes = element.className.split(/\s+/);
var length = classes.length;
var i = 0;
for (; i < length; i++) {
if (classes[i] === className) {
classes.splice(i, 1);
break;
}
}
// The className is not found
if (length === classes.length) {
classes.push(className);
}
element.className = classes.join(' ');
}
function toggleMenu() {
var active = 'active';
var elements = getElements();
toggleClass(elements.menu, active);
}
function handleEvent(e) {
var elements = getElements();
if (e.target.id === elements.menuLink.id) {
toggleMenu();
e.preventDefault();
} else if (elements.menu.className.indexOf('active') !== -1) {
toggleMenu();
}
}
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('click', handleEvent);
});
</script>
{% if nt %}
<label>
<input type="checkbox" class="alertCheckbox" autocomplete="off" />
<div class="alert {% if nl == 'error' %}error{% else %}success{% endif %}">
<span class="alertText">{{nt}}
<br class="clear"/></span>
</div>
</label>
{% endif %}
</body>
</html>
+105
View File
@@ -0,0 +1,105 @@
{% extends "base.html" %}
{% block content %}
<div id="list" class="pure-u-1-3" xmlns="http://www.w3.org/1999/html">
<div class="server-item pure-g">
<h1 class="server-content-title">Servers</h1>
</div>
{% for server in SERVERS %}
<div class="server-item server-item-{% if loop.index0 == selected_server|int %}unread{% else %}selected{% endif %} pure-g">
<div class="pure-u-3-4" onclick="location.href='/?selected_server={{loop.index0}}';">
<h5 class="server-name">{{ server.info()["name"] }}</h5>
<h4 class="server-info">{{ '/'.join(server.info()["url"].split('/')[0:-1]) }}</h4>
<h4 class="server-info">Port {{ server.info()["port_for_new_access_keys"] }}</h4>
<h4 class="server-info">Hostname {{ server.info()["hostname_for_access_keys"] }}</h4>
<h4 class="server-info">v.{{ server.info()["version"] }}</h4>
<p class="server-comment">
{{ server.info()["comment"] }}
</p>
</div>
</div>
{% endfor %}
<div onclick="location.href='/add_server';" class="server-item server-add pure-g">
<div class="pure-u-1">
+
</div>
</div>
</div>
{% if SERVERS %}
{% if server is none %}
{% set server = SERVERS[0] %}
{% else %}
{% set server = SERVERS[selected_server|int] %}
{% endif %}
<div id="main" class="pure-u-1-3">
<div class="server-content">
<div class="server-content-header pure-g">
<div class="pure-u-1-2">
<h1 class="server-content-title">{{server.info()["name"]}}</h1>
<p class="server-content-subtitle">
<span>v.{{server.info()["version"]}} {{server.info()["server_id"]}}</span>
</p>
</div>
<!-- <div class="server-content-controls pure-u-1-2">-->
<!-- <button class="secondary-button pure-button">Reply</button>-->
<!-- <button class="secondary-button pure-button">Forward</button>-->
<!-- <button class="secondary-button pure-button">Move to</button>-->
<!-- </div>-->
</div>
<div class="server-content-body">
<h3>Clients: {{ server.info()['keys']|length }}</h3>
<form class="pure-form pure-form-stacked" method="POST">
<fieldset>
<div class="pure-g">
<div class="pure-u-1 pure-u-md-1-3">
<label for="name">Server Name</br> Note that this will not be reflected on the devices of the users that you invited to connect to it.</label>
<input type="text" id="name" class="pure-u-23-24" name="name" value="{{server.info()['name']}}"/>
</div>
<div class="pure-u-1 pure-u-md-1-3">
<label for="url">Server URL</label>
<input type="text" readonly id="url" class="pure-u-23-24" name="url" value="{{server.info()['url']}}"/>
</div>
<div class="pure-u-1 pure-u-md-1-3">
<label for="comment">Comment</label>
<input type="text" id="comment" class="pure-u-23-24" name="comment" value="{{server.info()['comment']}}"/>
</div>
<div class="pure-u-1 pure-u-md-1-3">
<label for="port_for_new_access_keys">Port For New Access Keys</label>
<input type="text" id="port_for_new_access_keys" class="pure-u-23-24" name="port_for_new_access_keys" value="{{server.info()['port_for_new_access_keys']}}"/>
</div>
<div class="pure-u-1 pure-u-md-1-3">
<label for="hostname_for_access_keys">Hostname For Access Keys</label>
<input type="text" id="hostname_for_access_keys" class="pure-u-23-24" name="hostname_for_access_keys" value="{{server.info()['hostname_for_access_keys']}}"/>
</div>
<div class="pure-u-1 pure-u-md-1-3">
<label for="cert">Server Access Certificate</label>
<input type="text" readonly id="cert" class="pure-u-23-24" name="cert" value="{{server.info()['cert']}}"/>
</div>
<div class="pure-u-1 pure-u-md-1-3">
<label for="created_timestamp_ms">Created</label>
<input type="text" readonly id="created_timestamp_ms" class="pure-u-23-24" name="created_timestamp_ms" value="{{format_timestamp(server.info()['created_timestamp_ms']) }}"/>
</div>
<input type="hidden" readonly id="server_id" class="pure-u-23-24" name="server_id" value="{{server.info()['server_id']}}"/>
</div>
<p>Share anonymous metrics</p>
<label for="metrics_enabled" class="pure-radio">
<input type="radio" id="metrics_enabled" name="metrics" value="True" {% if server.info()['metrics_enabled'] == True %}checked{% endif %} /> Enable
</label>
<label for="metrics_disabled" class="pure-radio">
<input type="radio" id="metrics_disabled" name="metrics" value="False" {% if server.info()['metrics_enabled'] == False %}checked{% endif %} /> Disable
</label>
<button type="submit" class="pure-button pure-button-primary">Submit</button>
</fieldset>
</form>
</div>
</div>
</div>
{% endif %}
{% endblock %}