diff --git a/TODO b/TODO
index 145e2fca9b7947aba4b08ab6863ef5799e6baccc..8b7b2ec688e3a64016048d4cb7fbb343297cd054 100644
--- a/TODO
+++ b/TODO
@@ -7,3 +7,5 @@
   další instance, které mají stejné id, ale jiné uuid?)
 - hooky na create/delete user
 - přehlednější log
+- type=1 ignorovat (nestěžovat si, že je unknown)
+- indexy
diff --git a/app/flask b/app/flask
new file mode 100755
index 0000000000000000000000000000000000000000..b567abf5a828def9c9bbf54d40dda7712cc6d946
--- /dev/null
+++ b/app/flask
@@ -0,0 +1,5 @@
+#!/bin/sh
+export FLASK_APP=zoom.py
+export FLASK_ENV=development
+flask "$@"
+
diff --git a/app/templates/main.html b/app/templates/main.html
new file mode 100644
index 0000000000000000000000000000000000000000..222421361bf87075af9b016f678d1f8fce0e42e1
--- /dev/null
+++ b/app/templates/main.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<style>
+		#heading {
+			position: relative;
+		}
+		#schedule {
+			position: relative;
+			top: 2.5ex;
+		}
+		.room {
+			border: 1px solid green;
+		}
+		.hour {
+			border-bottom: 1px solid green;
+		}
+		.roomhead p {
+			text-align: center;
+			font-weight: bold;
+			margin-top: 0;
+		}
+		.meeting {
+			border: 1px solid blue;
+			font-size: smaller;
+			background-color: #ccccff;
+		}
+
+	</style>
+</head>
+<body>
+	<h1>MFF Zoom</h1>
+	<form method=GET action="?">
+		<label for=date>Date:</label>
+		<input id=date type=date name=date value="{{ g.date }}">
+		<select name=hours>
+			<option value=0{{ " selected" if g.hours==0 else "" }}>Working hours</option>
+			<option value=1{{ " selected" if g.hours==1 else "" }}>Whole day</option>
+		</select>
+		<input type=submit name=submit value="Submit">
+	</form>
+
+	<h2>Schedule for {{ g.dow }} {{ g.date }}</h2>
+
+	<div id=heading>
+{% for r in g.rooms %}
+		<div class=roomhead style='position: absolute; left: {{ r.x }}px; top: 0px; width: {{ r.w }}px;'>
+			<p>{{ r.name }}</p>
+		</div>
+{% endfor %}
+	</div>
+	<div id=schedule>
+{% for r in g.rooms %}
+		<div class=room style='position: absolute; left: {{ r.x }}px; top: 0px; width: {{ r.w }}px; height: {{ r.h }}px;'></div>
+{% endfor %}
+{% for h in g.hours %}
+		<div class=hour style='position: absolute; left: {{ h.x }}px; top: {{ h.y }}px; width: {{ h.w }}px; height: {{ h.h }}px;'></div>
+{% endfor %}
+{% for m in g.meetings %}
+		<div class=meeting style='position: absolute; left: {{ m.x }}px; top: {{ m.y }}px; width: {{ m.w }}px; height: {{ m.h }}px;' title='{{ m.topic|e }}'>
+			{{ m.start }} – {{ m.end }}
+		</div>
+{% endfor %}
+	</div>
+</body>
+</html>
diff --git a/app/zoom.py b/app/zoom.py
new file mode 100644
index 0000000000000000000000000000000000000000..c157106da3b729c86c69aae5f7734aad1aa2de96
--- /dev/null
+++ b/app/zoom.py
@@ -0,0 +1,137 @@
+import json
+from flask import Flask, render_template, request, g
+import psycopg2
+import psycopg2.extras
+import time
+import sys
+
+### Flask app object ###
+
+app = Flask(__name__)
+app.config.from_pyfile('config.py')
+
+### Database connection ###
+
+db_connection = None
+db = None
+
+# XXX: This is safe only because we never write to the database. Otherwise, we would
+# have to handle transactions and roll them back if an exception occurs during
+# processing of the request.
+
+def db_connect():
+    global db_connection, db
+    db_connection = psycopg2.connect(
+                host = 'localhost',
+                user = app.config['DB_USER'],
+                password = app.config['DB_PASSWD'],
+                dbname = app.config['DB_NAME'],
+            )
+    db = db_connection.cursor(cursor_factory=psycopg2.extras.NamedTupleCursor)
+
+def db_query(query, args=()):
+    if db is None:
+        db_connect()
+    try:
+        db.execute(query, args)
+    except psycopg2.DatabaseError:
+        # Reconnect if the connection died (timeout, server restart etc.)
+        db_connect()
+        db.execute(query, args)
+
+### Schedule ###
+
+def get_date():
+    try:
+        d = request.args.get('date', "")
+        return time.strptime(d, "%Y-%m-%d")
+    except ValueError:
+        return time.localtime()
+
+rooms = [
+    ('Z1', 'zoom-1@d3s.mff.cuni.cz'),
+    ('Z2', 'zoom-2@d3s.mff.cuni.cz'),
+    ('Z3', 'zoom-3@d3s.mff.cuni.cz'),
+    ('Z4', 'zoom-4@d3s.mff.cuni.cz'),
+    ('Z5', 'zoom-5@d3s.mff.cuni.cz'),
+    ('Z6', 'zoom-6@d3s.mff.cuni.cz'),
+    ('Z7', 'zoom-7@d3s.mff.cuni.cz'),
+    ('Z8', 'zoom-8@d3s.mff.cuni.cz'),
+]
+
+@app.route('/')
+def main_page():
+    dt = get_date()
+    date = time.strftime("%Y-%m-%d", dt)
+    t = time.mktime(dt)
+    g.date = date
+    g.dow = time.strftime("%A", dt)
+
+    hours_arg = request.args.get("hours", "")
+    if hours_arg in ["0", "1"]:
+        g.hours = int(hours_arg)
+    else:
+        g.hours = 0
+
+    if g.hours == 0:
+        hour_min = 8
+        hour_max = 24
+    else:
+        hour_min = 0
+        hour_max = 24
+    num_hours = hour_max - hour_min
+
+    num_rooms = len(rooms)
+    email_to_room_index = { rooms[i][1]: i for i in range(num_rooms) }
+    room_box_width = 100
+    room_hour_height = 50
+    g.total_width = room_box_width * num_rooms
+    g.total_height = num_hours * room_hour_height
+    g.rooms = [{
+            "x": i * room_box_width + 1,
+            "w": room_box_width - 1,
+            "h": g.total_height - 1,
+            "name": rooms[i][0],
+        } for i in range(num_rooms)]
+
+    g.hours = [{
+            "x": 1,
+            "y": i * room_hour_height + 1,
+            "w": num_rooms * room_box_width - 1,
+            "h": room_hour_height - 1,
+        } for i in range(num_hours)]
+
+    # XXX: No meeting is ever longer than 24 hours
+    db_query("""
+            SELECT m.meeting_id, m.topic, m.start_time, m.duration, u.email, u.full_name
+            FROM zoom_meetings m
+            JOIN zoom_users u ON u.id = m.host_id
+            WHERE start_time >= DATE %s - INTERVAL '1 day'
+              AND start_time <  DATE %s + INTERVAL '1 day'
+        """,
+        (date, date))
+
+    g.meetings = []
+    for r in db.fetchall():
+        i = email_to_room_index.get(r.email, -1)
+        if i < 0:
+            continue
+        start_t = int(r.start_time.timestamp())
+        end_t = start_t + r.duration*60
+        rel_start = start_t - t - hour_min*3600
+        rel_end = rel_start + r.duration*60
+        start = max(0, int(rel_start))
+        end = min(num_hours * 3600, int(rel_end))
+        app.logger.debug("Meeting: %s start=%s end=%s room_i=%s", r, start, end, i)
+        if start < end:
+            g.meetings.append({
+                "x": i * room_box_width + 4,
+                "y": int(start / 3600. * room_hour_height),
+                "w": room_box_width - 7,
+                "h": int((end - start) / 3600 * room_hour_height),
+                "start": time.strftime("%H:%M", time.localtime(start_t)),
+                "end": time.strftime("%H:%M", time.localtime(end_t)),
+                "topic": r.topic,
+            })
+
+    return render_template('main.html')
diff --git a/requirements.txt b/requirements.txt
index 699adcf9e2a3e72aaec9dedad79eb3c176472681..6e41d4050c28b722c2a96e4f228b8d3d1ccfc835 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
 zoomus
 psycopg2
 python-dateutil
+Flask