Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ dependencies = [
"pydantic>=2.7",
"httpx>=0.27",
"polyfactory>=3.2.0",
"flask-wtf>=1.2.2",
]


Expand Down
63 changes: 57 additions & 6 deletions src/critic/app.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import logging
import os
from uuid import UUID

from flask import Flask
from flask import Flask, flash, redirect, render_template, request, url_for

from critic.models import UptimeMonitorModel
from critic.tables import UptimeMonitorTable


log = logging.getLogger()

app = Flask(__name__)


@app.route('/')
def hello_world():
return '<p>Hello, <strong>World</strong>!</p>'
app.config['SECRET_KEY'] = os.environ.get('FLASK_SECRET_KEY', 'dev-change-me')


@app.route('/log')
Expand All @@ -26,3 +27,53 @@ def logs_example():
@app.route('/error')
def error():
raise RuntimeError('Deliberate runtime error')


@app.route('/login')
def login():
"""login with level 12 Auth"""
return render_template('login.html')


@app.route('/')
def dashboard():
"""Overview of Monitors"""
monitors = sorted(
UptimeMonitorTable.scan(),
key=lambda monitor: (str(monitor.project_id), monitor.slug),
)
table_name = UptimeMonitorTable.name()
log.warning('Dashboard loading monitors from %s, count=%s', table_name, len(monitors))
return render_template('dashboard.html', monitors=monitors, table_name=table_name)


@app.route('/create-monitor', methods=['GET', 'POST'])
def create_monitor():
"""Create a monitor."""
if request.method == 'POST':
project_id = request.form['project_id']
slug = request.form['slug']
url = request.form['url']
frequency_mins = int(request.form['frequency_mins'])

monitor = UptimeMonitorModel(
project_id=UUID(project_id),
slug=slug,
url=url,
frequency_mins=frequency_mins,
)
UptimeMonitorTable.put(monitor)

flash('Monitor created successfully!', 'success')
return redirect(url_for('dashboard'))
return render_template('create_monitor.html')


@app.route('/logout')
def logout():
"""This is temporary"""
return redirect(url_for('dashboard'))


if __name__ == '__main__':
app.run(debug=True)
8 changes: 8 additions & 0 deletions src/critic/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired, Length


class CreateProjectForm(FlaskForm):
name = StringField('Project Name', validators=[DataRequired(), Length(max=120)])
submit = SubmitField('Create Project')
7 changes: 7 additions & 0 deletions src/critic/libs/ddb.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,13 @@ def query(cls, partition_value: Any) -> list[BaseModel]:
items = response.get('Items', [])
return [cls.model(**deserialize(item)) for item in items]

@classmethod
def scan(cls) -> list[BaseModel]:
"""Return all items in the table."""
response = get_client().scan(TableName=cls.name())
items = response.get('Items', [])
return [cls.model(**deserialize(item)) for item in items]

@staticmethod
def alias(data: dict, val_suffix: str = '') -> tuple[dict, dict, list]:
"""
Expand Down
41 changes: 41 additions & 0 deletions src/critic/templates/base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Critic{% endblock %}</title>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
>
</head>
<body>

<header class="border-bottom bg-white">
<nav class="container py-3 d-flex align-items-center justify-content-between">
<div>
<div class="h5 mb-1">Level 12 Monitor</div>
<div class="text-secondary small">Monitor Dashboard</div>
</div>
<a class="text-decoration-none text-dark" href="/logout">Logout</a>
</nav>
</header>

<main class="container py-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} mb-3" role="alert">
{{ message }}
</div>
{% endfor %}
{% endif %}
{% endwith %}

{% block content %}{% endblock %}
</main>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>


</body>
</html>
67 changes: 67 additions & 0 deletions src/critic/templates/create_monitor.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{% extends "base.html" %}

{% block title %}Create Monitor{% endblock %}

{% block content %}
<div class="container py-4" style="max-width: 700px;">
<h1 class="h3 mb-4">Create Monitor</h1>

<form method="POST" action="{{ url_for('create_monitor') }}" class="vstack gap-3">
<div>
<label for="project_id" class="form-label">Project ID (UUID)</label>
<input
type="text"
class="form-control"
id="project_id"
name="project_id"
placeholder="6033aa47-a9f7-4d7f-b7ff-a11ba9b34474"
required
>
</div>

<div>
<label for="slug" class="form-label">Slug</label>
<input
type="text"
class="form-control"
id="slug"
name="slug"
placeholder="my-monitor"
required
>
<div class="form-text">Lowercase letters, numbers, and hyphens.</div>
</div>

<div>
<label for="url" class="form-label">URL</label>
<input
type="url"
class="form-control"
id="url"
name="url"
placeholder="https://example.com/health"
required
>
</div>

<div>
<label for="frequency_mins" class="form-label">Frequency (minutes)</label>
<input
type="number"
class="form-control"
id="frequency_mins"
name="frequency_mins"
min="1"
step="1"
value="5"
required
>
</div>

<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">Create Monitor</button>
<a href="{{ url_for('dashboard') }}" class="btn btn-outline-secondary">Cancel</a>
</div>
</form>
</div>
{% endblock %}
9 changes: 9 additions & 0 deletions src/critic/templates/create_project.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends "base.html" %}

{% block title %}Create Project{% endblock %}

{% block content %}



{% endblock %}
47 changes: 47 additions & 0 deletions src/critic/templates/dashboard.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{% extends "base.html" %}

{% block title %}Dashboard{% endblock %}

{% block content %}

<div class="d-flex justify-content-end mb-4">
<a class="btn btn-primary" href="{{ url_for('create_monitor') }}">Create Monitor</a>
</div>

{% if monitors %}
<p class="text-muted mb-3">
{{ monitors|length }} monitor{% if monitors|length != 1 %}s{% endif %}
</p>

<div class="table-responsive">
<table class="table align-middle">
<thead>
<tr>
<th scope="col">Slug</th>
<th scope="col">URL</th>
<th scope="col">Frequency</th>
<th scope="col">State</th>
<th scope="col">Project ID</th>
</tr>
</thead>
<tbody>
{% for monitor in monitors %}
<tr>
<td class="fw-semibold">{{ monitor.slug }}</td>
<td><a href="{{ monitor.url }}" target="_blank" rel="noreferrer">{{ monitor.url }}</a></td>
<td>{{ monitor.frequency_mins }} min</td>
<td class="text-capitalize">{{ monitor.state.value }}</td>
<td class="text-muted small">{{ monitor.project_id }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="border rounded p-4 bg-light">
<p class="mb-3 text-muted">No monitors yet.</p>
<a class="btn btn-primary" href="{{ url_for('create_monitor') }}">Create your first monitor</a>
</div>
{% endif %}

{% endblock %}
7 changes: 7 additions & 0 deletions src/critic/templates/delete.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends "base.html" %}

{% block title %}Delete Project{% endblock %}

{% block content %}

{% endblock %}
13 changes: 13 additions & 0 deletions src/critic/templates/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>



</body>
</html>
34 changes: 34 additions & 0 deletions tests/critic_tests/test_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import os

import pytest

from critic.app import app
from critic.libs.ddb import deserialize, get_client
from critic.tables import ProjectTable


os.environ.setdefault('CRITIC_NAMESPACE', 'test')


@pytest.fixture
def client():
app.config.update(
TESTING=True,
WTF_CSRF_ENABLED=False,
)
with app.test_client() as client:
yield client


def test_create_project_saves_to_ddb(client):
resp = client.post('/create', data={'name': 'My Project'})

assert resp.status_code == 302

response = get_client().scan(TableName=ProjectTable.name())
items = [deserialize(item) for item in response.get('Items', [])]

print(items)

assert len(items) == 1
assert items[0]['name'] == 'My Project'
28 changes: 28 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading