metrics are working!!!

This commit is contained in:
2020-05-13 00:28:53 -05:00
parent 7337375ae8
commit 30464ac8e5
25 changed files with 601 additions and 81 deletions

View File

@ -24,6 +24,8 @@ app.config.from_mapping(
MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD", default=""),
MAIL_DEFAULT_SENDER=os.environ.get("MAIL_DEFAULT_SENDER", default="forest@nullhex.com"),
PROMETHEUS_URL=os.environ.get("PROMETHEUS_URL", default="https://prometheus.cyberia.club"),
STRIPE_API_VERSION=os.environ.get("STRIPE_API_VERSION", default="2020-03-02"),
STRIPE_SECRET_KEY=os.environ.get("STRIPE_SECRET_KEY", default=""),
STRIPE_PUBLISHABLE_KEY=os.environ.get("STRIPE_PUBLISHABLE_KEY", default="")
@ -40,12 +42,14 @@ from capsulflask import db
db.init_app(app)
from capsulflask import auth, landing, console, payment_stripe
from capsulflask import auth, landing, console, payment_stripe, payment_btcpay, metrics
app.register_blueprint(landing.bp)
app.register_blueprint(auth.bp)
app.register_blueprint(console.bp)
app.register_blueprint(payment_stripe.bp)
app.register_blueprint(payment_btcpay.bp)
app.register_blueprint(metrics.bp)
app.add_url_rule("/", endpoint="index")

View File

@ -12,8 +12,8 @@ from flask_mail import Message
from werkzeug.exceptions import abort
from nanoid import generate
from capsulflask.metrics import durations as metric_durations
from capsulflask.auth import account_required
from capsulflask.db import get_model, my_exec_info_message
bp = Blueprint("console", __name__, url_prefix="/console")
@ -53,7 +53,7 @@ def index():
ipv4=(x['ipv4'] if x['ipv4'] else "..booting.."),
ipv4_status=("ok" if x['ipv4'] else "waiting-pulse"),
os=x['os'],
created=x['created'].strftime("%b %d %Y %H:%M")
created=x['created'].strftime("%b %d %Y")
),
vms
))
@ -63,6 +63,11 @@ def index():
@bp.route("/<string:id>")
@account_required
def detail(id):
duration=request.args.get('duration')
if not duration:
duration = "5m"
vm = get_model().get_vm_detail(email=session["account"], id=id)
if vm is None:
@ -72,7 +77,12 @@ def detail(id):
vm["created"] = vm['created'].strftime("%b %d %Y %H:%M")
vm["ssh_public_keys"] = ", ".join(vm["ssh_public_keys"]) if len(vm["ssh_public_keys"]) > 0 else "<deleted>"
return render_template("capsul-detail.html", vm=vm)
return render_template(
"capsul-detail.html",
vm=vm,
durations=list(map(lambda x: x.strip("_"), metric_durations.keys())),
duration=duration
)
@bp.route("/create", methods=("GET", "POST"))

View File

@ -27,7 +27,11 @@ def init_app(app):
schemaMigrationsPath = join(app.root_path, 'schema_migrations')
print("loading schema migration scripts from {}".format(schemaMigrationsPath))
for filename in listdir(schemaMigrationsPath):
key = re.search(r"^\d+_(up|down)", filename).group()
result = re.search(r"^\d+_(up|down)", filename)
if not result:
print(f"schemaVersion {filename} must match ^\d+_(up|down). exiting.")
exit(1)
key = result.group()
with open(join(schemaMigrationsPath, filename), 'rb') as file:
schemaMigrations[key] = file.read().decode("utf8")

View File

@ -111,6 +111,10 @@ class DBModel:
)
self.connection.commit()
# def vm_exists(self, email, id):
# self.cursor.execute("SELECT id FROM vms WHERE email = %s AND id = %s ", (email, id))
# return len(self.cursor.fetchall()) > 0
def get_vm_detail(self, email, id):
self.cursor.execute("""
SELECT vms.id, vms.last_seen_ipv4, vms.last_seen_ipv6, os_images.description, vms.created, vms.deleted,

View File

@ -1,16 +1,5 @@
from flask import Blueprint
from flask import flash
from flask import current_app
from flask import g
from flask import redirect
from flask import url_for
from flask import request
from flask import session
from flask import render_template
from flask_mail import Message
from werkzeug.exceptions import abort
from capsulflask.db import get_model
bp = Blueprint("landing", __name__, url_prefix="/")

234
capsulflask/metrics.py Normal file
View File

@ -0,0 +1,234 @@
import matplotlib.ticker as ticker
import matplotlib.pyplot as pyplot
import matplotlib.dates as mdates
import requests
#import json
from datetime import datetime
from io import BytesIO
from flask import Blueprint
from flask import current_app
from flask import session
from flask import render_template, make_response
from werkzeug.exceptions import abort
from capsulflask.db import get_model
from capsulflask.auth import account_required
bp = Blueprint("metrics", __name__, url_prefix="/metrics")
durations = dict(
_5m=[60*5, 15],
_1h=[60*60, 60],
_1d=[60*60*24, 60*20],
_30d=[60*60*24*30, 60*300]
)
sizes = dict(
s=[0.77, 0.23, 4],
m=[1, 1, 2],
l=[6, 4, 1],
)
green = (121/255, 240/255, 50/255)
blue = (70/255, 150/255, 255/255)
red = (255/255, 50/255, 8/255)
@bp.route("/html/<string:metric>/<string:capsulid>/<string:duration>")
@account_required
def display_metric(metric, capsulid, duration):
vm = get_model().get_vm_detail(session["account"], capsulid)
return render_template(
"display-metric.html",
vm=vm,
duration=duration,
durations=list(map(lambda x: x.strip("_"), durations.keys())),
metric=metric
)
@bp.route("/<string:metric>/<string:capsulid>/<string:duration>/<string:size>")
@account_required
def metric_png(metric, capsulid, duration, size):
result = get_plot_bytes(metric, capsulid, duration, size)
if result[0] != 200:
abort(result[0])
response = make_response(result[1])
response.headers.set('Content-Type', 'image/png')
return response
def get_plot_bytes(metric, capsulid, duration, size):
duration = f"_{duration}"
if duration not in durations:
return (404, None)
if size not in sizes:
return (404, None)
vm = get_model().get_vm_detail(session["account"], capsulid)
if not vm:
return (404, None)
now_unix = int(datetime.strftime(datetime.now(), "%s"))
duration_seconds = durations[duration][0]
interval_seconds = durations[duration][1] * sizes[size][2]
if interval_seconds < 30:
interval_seconds = 30
metric_queries = dict(
cpu=f"irate(libvirtd_domain_info_cpu_time_seconds_total{{domain='{capsulid}'}}[30s])",
memory=f"libvirtd_domain_info_memory_usage_bytes{{domain='{capsulid}'}}",
network_in=f"rate(libvirtd_domain_interface_stats_receive_bytes_total{{domain='{capsulid}'}}[{interval_seconds}s])",
network_out=f"rate(libvirtd_domain_interface_stats_transmit_bytes_total{{domain='{capsulid}'}}[{interval_seconds}s])",
disk=f"rate(libvirtd_domain_block_stats_read_bytes_total{{domain='{capsulid}'}}[{interval_seconds}s])%2Brate(libvirtd_domain_block_stats_write_bytes_total{{domain='{capsulid}'}}[{interval_seconds}s])",
)
scales = dict(
cpu=vm["vcpus"],
memory=vm["memory_mb"]*1024*1024,
network_in=1024*1024*2,
network_out=1024*200,
disk=1024*1024*2,
)
if metric not in metric_queries:
return (404, None)
range_and_interval = f"start={now_unix-duration_seconds}&end={now_unix}&step={interval_seconds}"
prometheus_range_url = f"{current_app.config['PROMETHEUS_URL']}/api/v1/query_range"
#print(f"{prometheus_range_url}?query={metric_queries[metric]}&{range_and_interval}")
prometheus_response = requests.get(f"{prometheus_range_url}?query={metric_queries[metric]}&{range_and_interval}")
if prometheus_response.status_code >= 300:
return (502, None)
time_series_data = list(map(
lambda x: (datetime.fromtimestamp(x[0]), float(x[1])),
prometheus_response.json()["data"]["result"][0]["values"]
))
plot_bytes = draw_plot_bytes(time_series_data, scale=scales[metric], size_x=sizes[size][0], size_y=sizes[size][1])
return (200, plot_bytes)
def draw_plot_bytes(data, scale, size_x=3, size_y=1):
pyplot.style.use("seaborn-dark")
fig, my_plot = pyplot.subplots(figsize=(size_x, size_y))
# x=range(1, 15)
# y=[1,4,6,8,4,5,3,2,4,1,5,6,8,7]
divide_by = 1
unit = ""
if scale > 1024 and scale < 1024*1024*1024:
divide_by = 1024*1024
unit = "MB"
if scale > 1024*1024*1024:
divide_by = 1024*1024*1024
unit = "GB"
scale /= divide_by
if scale > 10:
my_plot.get_yaxis().set_major_formatter( ticker.FuncFormatter(lambda x, p: "{}{}".format(int(x), unit)) )
elif scale > 1:
my_plot.get_yaxis().set_major_formatter( ticker.FuncFormatter(lambda x, p: "{:.1f}{}".format(x, unit)) )
else:
my_plot.get_yaxis().set_major_formatter( ticker.FuncFormatter(lambda x, p: "{:.2f}{}".format(x, unit)) )
x=list(map(lambda x: x[0], data))
y=list(map(lambda x: x[1]/divide_by, data))
minutes = float((x[len(x)-1] - x[0]).total_seconds())/float(60)
hours = minutes/float(60)
days = hours/float(24)
day_locator = mdates.WeekdayLocator()
minute_locator = mdates.MinuteLocator()
ten_minute_locator = mdates.MinuteLocator(interval=10)
hour_locator = mdates.HourLocator(interval=6)
hour_minute_formatter = mdates.DateFormatter('%H:%M')
day_formatter = mdates.DateFormatter('%b %d')
if minutes < 10:
my_plot.xaxis.set_major_locator(minute_locator)
my_plot.xaxis.set_major_formatter(hour_minute_formatter)
elif hours < 2:
my_plot.xaxis.set_major_locator(ten_minute_locator)
my_plot.xaxis.set_major_formatter(hour_minute_formatter)
elif days < 2:
my_plot.xaxis.set_major_locator(hour_locator)
my_plot.xaxis.set_major_formatter(hour_minute_formatter)
else:
my_plot.xaxis.set_major_locator(day_locator)
my_plot.xaxis.set_major_formatter(day_formatter)
average=(sum(y)/len(y))/scale
average=average*1.25+0.1
bg_color=color_gradient(average)
average -= 0.1
fill_color=color_gradient(average)
highlight_color=lerp_rgb_tuples(fill_color, (1,1,1), 0.5)
my_plot.fill_between( x, scale, color=bg_color, alpha=0.13)
my_plot.fill_between( x, y, color=highlight_color, alpha=0.3)
my_plot.plot(x, y, 'r-', color=highlight_color)
my_plot.patch.set_facecolor('red')
my_plot.patch.set_alpha(0.5)
if size_y < 4:
my_plot.set_yticks([0, scale*0.5, scale])
my_plot.set_ylim(0, scale)
my_plot.xaxis.label.set_color(highlight_color)
my_plot.tick_params(axis='x', colors=highlight_color)
my_plot.yaxis.label.set_color(highlight_color)
my_plot.tick_params(axis='y', colors=highlight_color)
if size_x < 4:
my_plot.set_xticklabels([])
if size_y < 1:
my_plot.set_yticklabels([])
image_binary = BytesIO()
fig.savefig(image_binary, transparent=True, bbox_inches="tight", pad_inches=0.05)
return image_binary.getvalue()
def lerp_rgb_tuples(a, b, lerp):
if lerp < 0:
lerp = 0
if lerp > 1:
lerp = 1
return (
a[0]*(1.0-lerp)+b[0]*lerp,
a[1]*(1.0-lerp)+b[1]*lerp,
a[2]*(1.0-lerp)+b[2]*lerp
)
def color_gradient(value):
if value < 0:
value = 0
if value > 1:
value = 1
if value < 0.5:
return lerp_rgb_tuples(green, blue, value*2)
else:
return lerp_rgb_tuples(blue, red, (value-0.5)*2)

View File

@ -0,0 +1,12 @@
from flask import Blueprint
from flask import render_template
from capsulflask.db import get_model
from capsulflask.auth import account_required
bp = Blueprint("btcpay", __name__, url_prefix="/btcpay")
@bp.route("/")
@account_required
def index():
return render_template("btcpay.html")

View File

@ -92,4 +92,7 @@ VALUES ('forest.n.johnson@gmail.com');
INSERT INTO payments (email, dollars, created)
VALUES ('forest.n.johnson@gmail.com', 20.00, TO_TIMESTAMP('2020-05-05','YYYY-MM-DD'));
INSERT INTO vms (id, email, os, size)
VALUES ('capsul-yi9ffqbjly', 'forest.n.johnson@gmail.com', 'alpine311', 'f1-xx');
UPDATE schemaversion SET version = 2;

View File

@ -61,12 +61,15 @@ main {
}
.full-margin {
width: 100%;
margin: 3rem 0;
}
.half-margin {
width: 100%;
margin: 1.5rem 0;
}
.third-margin {
width: 100%;
margin: 1rem 0;
}
@ -79,6 +82,12 @@ main {
.wrap {
flex-wrap: wrap;
}
.row.grid-large > div {
flex: 1 1 20em;
}
.row.grid-small > div {
flex: 0 0 8em;
}
.justify-start {
justify-content: flex-start;
}
@ -163,6 +172,12 @@ input[type=number] {
-moz-appearance: textfield;
}
input[type=image].submit {
background-color: #0000;
}
h1, h2, h3, h4, h5 {
font-size:calc(0.40rem + 1vmin);
margin: initial;
@ -174,6 +189,10 @@ ul li {
margin: 0.5em 0;
}
hr {
width: 100%;
}
table{
border-collapse: collapse;
}
@ -188,6 +207,15 @@ table.small td, table.small th {
font: calc(0.35rem + 0.83vmin) monospace;
padding: 0.1em 1em;
}
table.small td.metrics {
padding: 0;
}
td.metrics img {
margin-left: -20px;
margin-right: -20px;
margin-top: -5px;
margin-bottom: -5px;
}
th {
border-right: 4px solid #241e1e;
text-align: left;
@ -204,6 +232,13 @@ td {
to { color: rgba(221, 169, 56, 0.2); }
}
div.metric {
display: flex;
align-items: last baseline;
flex-direction: column;
margin: 0.7em;
}
.code {
display: inline-block;
padding: 0.5em 1.2em;

View File

@ -3,7 +3,7 @@
{% block title %}Account Balance{% endblock %}
{% block content %}
<div class="third-margin">
<div class="row third-margin">
<h1>Account Balance: ${{ account_balance }}</h1>
</div>
<div class="half-margin">
@ -11,7 +11,7 @@
<div class="row">
{% if has_payments %}
<div>
<div class="third-margin">
<div class="row third-margin">
<h1>Payments</h1>
</div>
@ -52,33 +52,36 @@
{% if has_vms %}
<div class="third-margin">
<div class="row third-margin">
<h1>Capsuls Billed</h1>
</div>
<table class="small">
<thead>
<tr>
<th>id</th>
<th>created</th>
<th>deleted</th>
<th>$/month</th>
<th>months</th>
<th>$ billed</th>
</tr>
</thead>
<tbody>
{% for vm in vms_billed %}
<div class="row">
<table class="small">
<thead>
<tr>
<td>{{ vm["id"] }}</td>
<td>{{ vm["created"] }}</td>
<td>{{ vm["deleted"] }}</td>
<td>${{ vm["dollars_per_month"] }}</td>
<td>{{ vm["months"] }}</td>
<td>${{ vm["dollars"] }}</td>
<th>id</th>
<th>created</th>
<th>deleted</th>
<th>$/month</th>
<th>months</th>
<th>$ billed</th>
</tr>
{% endfor %}
</tbody>
</table>
</thead>
<tbody>
{% for vm in vms_billed %}
<tr>
<td>{{ vm["id"] }}</td>
<td>{{ vm["created"] }}</td>
<td>{{ vm["deleted"] }}</td>
<td>${{ vm["dollars_per_month"] }}</td>
<td>{{ vm["months"] }}</td>
<td>${{ vm["dollars"] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,38 @@
{% extends 'base.html' %}
{% block title %}Pay with Cryptocurrency{% endblock %}
{% block content %}
<div class="row third-margin">
<h1>Pay with Cryptocurrency</h1>
</div>
<div class="row half-margin">
<form method="POST" action="https://btcpay.cyberia.club/api/v1/invoices">
<input type="hidden" name="storeId" value="FgYNGKEHKm2tBhwejo1zdSQ15DknPWvip2pXLKBv96wc">
<input type="hidden" name="currency" value="USD">
<div class="row">
<label for="btcpay-input-price">$</label>
<input
id="btcpay-input-price"
name="price"
type="number"
min="0"
max="2000"
oninput="event.preventDefault();isNaN(event.target.value) || event.target.value <= 0 ? document.querySelector('#btcpay-input-price').value = 0 : event.target.value"
/>
<input type="image" class="submit" name="submit"
src="https://btcpay.cyberia.club/img/paybutton/pay.svg"
style="width:168px"
alt="Pay with BtcPay"
/>
</div>
</form>
</div>
{% endblock %}
{% block pagesource %}/templates/btcpay.html{% endblock %}

View File

@ -3,10 +3,11 @@
{% block title %}{{ vm['id'] }}{% endblock %}
{% block content %}
<div class="third-margin">
<div class="row third-margin">
<h1>{{ vm['id'] }}</h1>
</div>
<div class="half-margin">
<div class="row wrap grid-large third-margin">
<div class="row justify-start">
<label class="align" for="created">Created</label>
<span id=created>{{ vm['created'] }}</span>
@ -47,7 +48,57 @@
<label class="align" for="ssh_public_keys">SSH Public Keys</label>
<a id=ssh_public_keys href="/console/ssh">{{ vm['ssh_public_keys'] }}</a>
</div>
</div>
<div class="row ">
<hr/>
</div>
<div class="row third-margin">
{% for d in durations %}
<a href="/console/{{ vm['id'] }}?duration={{ d }}">
{% if d == duration %}
<span class="code">{{ d }}</span>
{% else %}
{{ d }}
{% endif %}
</a>
{% endfor %}
</div>
<div class="row wrap grid-small justify-end">
<div class="metric">
<h1>cpu</h1>
<a href="/metrics/html/cpu/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/cpu/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</div>
<div class="metric">
<h1>memory</h1>
<a href="/metrics/html/memory/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/memory/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</div>
<div class="metric">
<h1>network_in</h1>
<a href="/metrics/html/network_in/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/network_in/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</div>
<div class="metric">
<h1>network_out</h1>
<a href="/metrics/html/network_out/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/network_out/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</div>
<div class="metric">
<h1>disk</h1>
<a href="/metrics/html/disk/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/disk/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</div>
</div>
{% endblock %}

View File

@ -3,7 +3,7 @@
{% block title %}Capsuls{% endblock %}
{% block content %}
<div class="third-margin">
<div class="row third-margin">
<h1>Capsuls</h1>
</div>
<div class="third-margin">
@ -11,30 +11,37 @@
<div class="row third-margin justify-end">
<a href="/console/create">Create Capsul</a>
</div>
<table>
<thead>
<tr>
<th>id</th>
<th>size</th>
<th>ipv4</th>
<th>os</th>
<th>created</th>
</tr>
</thead>
<tbody>
{% for vm in vms %}
<div class="row">
<table>
<thead>
<tr>
<td><a href="/console/{{ vm['id'] }}">{{ vm["id"] }}</a></td>
<td>{{ vm["size"] }}</td>
<td class="{{ vm['ipv4_status'] }}">{{ vm["ipv4"] }}</td>
<td>{{ vm["os"] }}</td>
<td>{{ vm["created"] }}</td>
<th>id</th>
<th>size</th>
<th>cpu</th>
<th>mem</th>
<th>ipv4</th>
<th>os</th>
<th>created</th>
</tr>
{% endfor %}
</tbody>
</table>
</thead>
<tbody>
{% for vm in vms %}
<tr>
<td><a href="/console/{{ vm['id'] }}">{{ vm["id"] }}</a></td>
<td>{{ vm["size"] }}</td>
<td class="metrics"><img src="/metrics/cpu/{{ vm['id'] }}/5m/s"/></td>
<td class="metrics"><img src="/metrics/memory/{{ vm['id'] }}/5m/s"/></td>
<td class="{{ vm['ipv4_status'] }}">{{ vm["ipv4"] }}</td>
<td>{{ vm["os"] }}</td>
<td>{{ vm["created"] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
You don't have any Capsuls running. <a href="/console/create">Create one</a> today!
<div class="row">You don't have any Capsuls running. <a href="/console/create">Create one</a> today!</div>
{% endif %}
</div>
{% endblock %}

View File

@ -3,7 +3,7 @@
{% block title %}Changelog{% endblock %}
{% block content %}
<div class="full-margin"><h1>CHANGELOG</h1></div>
<div class="row full-margin"><h1>CHANGELOG</h1></div>
{% endblock %}
{% block subcontent %}
<p>

View File

@ -3,10 +3,11 @@
{% block title %}Create{% endblock %}
{% block content %}
<div class="third-margin">
<div class="row third-margin">
<h1>Create Capsul</h1>
</div>
<div class="third-margin">
<div class="row third-margin"><div>
{% if created_os %}
<p>
Your Capsul was successfully created! You should already see it listed on the
@ -46,7 +47,8 @@
* net is calculated as a per-month average
* all VMs come standard with one public IPv4 addr</pre>
<pre>
Your <a href="/console/account-balance">account balance</a>: ${{ account_balance }}</div>
Your <a href="/console/account-balance">account balance</a>: ${{ account_balance }}
</pre>
<form method="post">
<div class="row justify-start">
@ -79,12 +81,13 @@
<input type="submit" value="Create">
</div>
</form>
</div>
{% endif %}
{% endif %}
{% endif %}
</div>
</div></div>
{% endblock %}
{% block subcontent %}

View File

@ -0,0 +1,31 @@
{% extends 'base.html' %}
{% block title %}{{ vm['id'] }} - {{ metric }} - {{ duration }}{% endblock %}
{% block content %}
<div class="row half-margin">
<h1>{{ vm['id'] }} - {{ metric }} - {{ duration }}</h1>
</div>
<div class="row third-margin">
{% for d in durations %}
<a href="/metrics/html/{{ metric }}/{{ vm['id'] }}/{{ d }}">
{% if d == duration %}
<span class="code">{{ d }}</span>
{% else %}
{{ d }}
{% endif %}
</a>
{% endfor %}
</div>
<div class="row">
<div>
<h1>cpu</h1>
<img src="/metrics/{{ metric }}/{{ vm['id'] }}/{{ duration }}/l"/>
</div>
</div>
{% endblock %}
{% block pagesource %}/templates/display-metric.html{% endblock %}

View File

@ -3,7 +3,7 @@
{% block title %}FAQ{% endblock %}
{% block content %}
<div class="full-margin"><h1>Frequently Asked Questions</h1></div>
<div class="row full-margin"><h1>Frequently Asked Questions</h1></div>
{% endblock %}
{% block subcontent %}

View File

@ -12,6 +12,7 @@
\ /
`"`
</pre>
<img src="/metrics/cpu/capsul-yi9ffqbjly/5m/l"/>
<span>Simple, fast, private compute by <a href="https://cyberia.club">cyberia.club</a></span>
{% endblock %}

View File

@ -3,7 +3,7 @@
{% block title %}check your email{% endblock %}
{% block content %}
<div class="full-margin">Check your email. A login link has been sent to {{ email }}</div>
<div class="row full-margin">Check your email. A login link has been sent to {{ email }}</div>
{% endblock %}
{% block pagesource %}/templates/login-landing.html{% endblock %}

View File

@ -3,7 +3,7 @@
{% block title %}login{% endblock %}
{% block content %}
<div class="half-margin">
<div class="row half-margin">
<h1>LOGIN</h1>
</div>
<form method="post" class="half-margin">

View File

@ -3,10 +3,10 @@
{% block title %}SSH Public Keys{% endblock %}
{% block content %}
<div class="third-margin">
<div class="row third-margin">
<h1>SSH PUBLIC KEYS</h1>
</div>
<div class="third-margin">
<div class="row third-margin"><div>
{% if has_ssh_public_keys %} <hr/> {% endif %}
{% for ssh_public_key in ssh_public_keys %}
@ -51,7 +51,7 @@
<input type="submit" value="Upload">
</div>
</form>
</div>
</div></div>
{% endblock %}
{% block pagesource %}/templates/ssh-public-keys.html{% endblock %}

View File

@ -1,11 +1,11 @@
{% extends 'base.html' %}
{% block title %}Capsuls{% endblock %}
{% block title %}Pay with Stripe{% endblock %}
{% block head %}<script src="https://js.stripe.com/v3/"></script>{% endblock %}
{% block content %}
<div class="third-margin">
<div class="row third-margin">
<h1>PAY WITH STRIPE</h1>
</div>
<div class="row half-margin">

View File

@ -3,10 +3,10 @@
{% block title %}Support{% endblock %}
{% block content %}
<div class="half-margin">
<div class="row half-margin">
<h1>SUPPORT</h1>
</div>
<div class="half-margin">
<div class="row half-margin">
<a href="mailto:support@cyberia.club?subject=Please%20halp!">support@cyberia.club</a>
</div>
{% endblock %}