From 17b60827b92f3e7b826ad79276a731eea670d148 Mon Sep 17 00:00:00 2001 From: Tim 'mithro' Ansell Date: Sun, 1 Mar 2026 11:20:21 +1100 Subject: [PATCH 1/3] Add board_type field to Pi model for multi-board support Add BOARD_TYPES choices (arty_a7, netv2, ulx3s, fomu, tt_asic, tt_fpga) and board_type CharField to Pi model. Update admin with list_display and list_filter. Update views to group boards by type on index and auto-select tt.html template for Tiny Tapeout boards. Add board_type to fixtures. Co-Authored-By: Claude Opus 4.6 --- .../pib/pibfpgas/fixtures/fpgas.online.json | 2 +- .../pibfpgas/fixtures/ps1.fpgas.online.json | 2 +- .../files/pib/pibfpgas/src/pibfpgas/admin.py | 5 ++-- .../pibfpgas/migrations/0002_pi_board_type.py | 30 +++++++++++++++++++ .../files/pib/pibfpgas/src/pibfpgas/models.py | 10 +++++++ .../files/pib/pibfpgas/src/pibfpgas/views.py | 29 +++++++++++++----- 6 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/migrations/0002_pi_board_type.py diff --git a/ansible/roles/site/files/pib/pibfpgas/fixtures/fpgas.online.json b/ansible/roles/site/files/pib/pibfpgas/fixtures/fpgas.online.json index 5f5bebb..4eb4396 100644 --- a/ansible/roles/site/files/pib/pibfpgas/fixtures/fpgas.online.json +++ b/ansible/roles/site/files/pib/pibfpgas/fixtures/fpgas.online.json @@ -1 +1 @@ -[{"model": "pibfpgas.pi", "pk": 1, "fields": {"port": 34, "mac": "b8:27:eb:6d:27:f6", "cable_color": "white", "location": "", "serial_no": "7a6d27f6"}}, {"model": "pibfpgas.pi", "pk": 2, "fields": {"port": 36, "mac": "b8:27:eb:86:39:63", "cable_color": "blue", "location": "", "serial_no": "80863963"}}, {"model": "pibfpgas.pi", "pk": 3, "fields": {"port": 40, "mac": "e4:5f:01:97:1f:7e", "cable_color": "gray", "location": "", "serial_no": "613a4524"}}, {"model": "pibfpgas.pi", "pk": 4, "fields": {"port": 42, "mac": "e4:5f:01:8d:f7:17", "model": "Raspberry_Pi_4_Model_B_Rev_1", "cable_color": "yellow", "location": "", "serial_no": "f77b8415"}}, {"model": "pibfpgas.pi", "pk": 5, "fields": {"port": 46, "mac": "e4:5f:01:97:32:d2", "cable_color": "gray/white", "location": "", "serial_no": "8483b266"}}, {"model": "pibfpgas.pi", "pk": 6, "fields": {"port": 48, "mac": "e4:5f:01:96:f8:a5", "cable_color": "blue", "location": "", "serial_no": "ce8e3593"}}] \ No newline at end of file +[{"model": "pibfpgas.pi", "pk": 1, "fields": {"port": 34, "mac": "b8:27:eb:6d:27:f6", "cable_color": "white", "location": "", "serial_no": "7a6d27f6", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 2, "fields": {"port": 36, "mac": "b8:27:eb:86:39:63", "cable_color": "blue", "location": "", "serial_no": "80863963", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 3, "fields": {"port": 40, "mac": "e4:5f:01:97:1f:7e", "cable_color": "gray", "location": "", "serial_no": "613a4524", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 4, "fields": {"port": 42, "mac": "e4:5f:01:8d:f7:17", "model": "Raspberry_Pi_4_Model_B_Rev_1", "cable_color": "yellow", "location": "", "serial_no": "f77b8415", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 5, "fields": {"port": 46, "mac": "e4:5f:01:97:32:d2", "cable_color": "gray/white", "location": "", "serial_no": "8483b266", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 6, "fields": {"port": 48, "mac": "e4:5f:01:96:f8:a5", "cable_color": "blue", "location": "", "serial_no": "ce8e3593", "board_type": "arty_a7"}}] diff --git a/ansible/roles/site/files/pib/pibfpgas/fixtures/ps1.fpgas.online.json b/ansible/roles/site/files/pib/pibfpgas/fixtures/ps1.fpgas.online.json index 93341d2..8686565 100644 --- a/ansible/roles/site/files/pib/pibfpgas/fixtures/ps1.fpgas.online.json +++ b/ansible/roles/site/files/pib/pibfpgas/fixtures/ps1.fpgas.online.json @@ -1 +1 @@ -[{"model": "pibfpgas.pi", "pk": 1, "fields": {"port": 2, "mac": "b8:27:eb:2f:5d:08", "model": "Raspberry_Pi_3_Model_B_Rev_1", "cable_color": "grey", "location": "front 1", "serial_no": "042f5d08"}}, {"model": "pibfpgas.pi", "pk": 2, "fields": {"port": 3, "mac": "dc:a6:32:05:32:45", "model": "Raspberry_Pi_4_Model_B_Rev_1", "cable_color": "white", "location": "front 2", "serial_no": "f1b7bb5a"}}, {"model": "pibfpgas.pi", "pk": 3, "fields": {"port": 5, "mac": "b8:27:eb:d4:f1:74", "model": "Raspberry_Pi_3_Model_B_Rev_1", "cable_color": "white", "location": "front 4", "serial_no": "9ed4f174"}}, {"model": "pibfpgas.pi", "pk": 4, "fields": {"port": 7, "mac": "b8:27:eb:33:51:27", "model": "Raspberry_Pi_3_Model_B_Plus_Rev_1", "cable_color": "blue", "location": "front 5", "serial_no": 48335127}}, {"model": "pibfpgas.pi", "pk": 5, "fields": {"port": 9, "mac": "b8:27:eb:a3:51:b4", "model": "Raspberry_Pi_3_Model_B_Plus_Rev_1", "cable_color": "blue", "location": "front 6", "serial_no": "8da351b4"}}, {"model": "pibfpgas.pi", "pk": 6, "fields": {"port": 11, "mac": "b8:27:eb:51:01:df", "model": "Raspberry_Pi_3_Model_B_Rev_1", "cable_color": "blue", "location": "front 7", "serial_no": "265101df"}}, {"model": "pibfpgas.pi", "pk": 7, "fields": {"port": 13, "mac": "b8:27:eb:68:fc:e7", "model": "Raspberry_Pi_3_Model_B_Rev_1", "cable_color": "white", "location": "front 8", "serial_no": "f168fce7"}}, {"model": "pibfpgas.pi", "pk": 8, "fields": {"port": 21, "mac": "b8:27:eb:5f:de:85", "model": "", "cable_color": "blue", "location": "back 7", "serial_no": "7d5fde85"}}, {"model": "pibfpgas.pi", "pk": 9, "fields": {"port": 23, "mac": "b8:27:eb:0c:f8:43", "model": "Raspberry_Pi_3_Model_B_Rev_1", "cable_color": "grey", "location": "back 8", "serial_no": "9b0cf843"}}] \ No newline at end of file +[{"model": "pibfpgas.pi", "pk": 1, "fields": {"port": 2, "mac": "b8:27:eb:2f:5d:08", "model": "Raspberry_Pi_3_Model_B_Rev_1", "cable_color": "grey", "location": "front 1", "serial_no": "042f5d08", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 2, "fields": {"port": 3, "mac": "dc:a6:32:05:32:45", "model": "Raspberry_Pi_4_Model_B_Rev_1", "cable_color": "white", "location": "front 2", "serial_no": "f1b7bb5a", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 3, "fields": {"port": 5, "mac": "b8:27:eb:d4:f1:74", "model": "Raspberry_Pi_3_Model_B_Rev_1", "cable_color": "white", "location": "front 4", "serial_no": "9ed4f174", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 4, "fields": {"port": 7, "mac": "b8:27:eb:33:51:27", "model": "Raspberry_Pi_3_Model_B_Plus_Rev_1", "cable_color": "blue", "location": "front 5", "serial_no": "48335127", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 5, "fields": {"port": 9, "mac": "b8:27:eb:a3:51:b4", "model": "Raspberry_Pi_3_Model_B_Plus_Rev_1", "cable_color": "blue", "location": "front 6", "serial_no": "8da351b4", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 6, "fields": {"port": 11, "mac": "b8:27:eb:51:01:df", "model": "Raspberry_Pi_3_Model_B_Rev_1", "cable_color": "blue", "location": "front 7", "serial_no": "265101df", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 7, "fields": {"port": 13, "mac": "b8:27:eb:68:fc:e7", "model": "Raspberry_Pi_3_Model_B_Rev_1", "cable_color": "white", "location": "front 8", "serial_no": "f168fce7", "board_type": "arty_a7"}}, {"model": "pibfpgas.pi", "pk": 8, "fields": {"port": 21, "mac": "b8:27:eb:5f:de:85", "model": "", "cable_color": "blue", "location": "back 7", "serial_no": "7d5fde85", "board_type": "tt_asic"}}, {"model": "pibfpgas.pi", "pk": 9, "fields": {"port": 23, "mac": "b8:27:eb:0c:f8:43", "model": "Raspberry_Pi_3_Model_B_Rev_1", "cable_color": "grey", "location": "back 8", "serial_no": "9b0cf843", "board_type": "arty_a7"}}] diff --git a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/admin.py b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/admin.py index e92ff19..bbef7c2 100644 --- a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/admin.py +++ b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin -from .models import * +from .models import Pi class PiAdmin(admin.ModelAdmin): - pass + list_display = ('port', 'board_type', 'location', 'model', 'cable_color') + list_filter = ('board_type',) admin.site.register(Pi, PiAdmin) diff --git a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/migrations/0002_pi_board_type.py b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/migrations/0002_pi_board_type.py new file mode 100644 index 0000000..dcd7c76 --- /dev/null +++ b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/migrations/0002_pi_board_type.py @@ -0,0 +1,30 @@ +# Generated by Django 6.0.2 on 2026-02-28 05:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("pibfpgas", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="pi", + name="board_type", + field=models.CharField( + blank=True, + choices=[ + ("arty_a7", "Arty A7"), + ("netv2", "NeTV2"), + ("ulx3s", "ULX3S"), + ("fomu", "Fomu"), + ("tt_asic", "Tiny Tapeout ASIC"), + ("tt_fpga", "Tiny Tapeout FPGA Emulation"), + ], + default="arty_a7", + max_length=20, + ), + ), + ] diff --git a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/models.py b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/models.py index 5ed49b8..cbe45a5 100644 --- a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/models.py +++ b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/models.py @@ -2,6 +2,15 @@ from django.db import models +BOARD_TYPES = [ + ('arty_a7', 'Arty A7'), + ('netv2', 'NeTV2'), + ('ulx3s', 'ULX3S'), + ('fomu', 'Fomu'), + ('tt_asic', 'Tiny Tapeout ASIC'), + ('tt_fpga', 'Tiny Tapeout FPGA Emulation'), +] + class Pi(models.Model): port = models.IntegerField() mac = models.CharField(max_length=17, blank=True) @@ -9,3 +18,4 @@ class Pi(models.Model): location = models.CharField(max_length=30, blank=True) model = models.CharField(max_length=30, blank=True) cable_color = models.CharField(max_length=10, blank=True) + board_type = models.CharField(max_length=20, choices=BOARD_TYPES, default='arty_a7', blank=True) diff --git a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/views.py b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/views.py index 5619f7c..4072319 100644 --- a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/views.py +++ b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/views.py @@ -1,36 +1,51 @@ # pibup - pib upload form -from base64 import b64encode +from itertools import groupby -from django.http import HttpResponseRedirect from django.shortcuts import render from django.shortcuts import get_object_or_404 from django.conf import settings -from .models import Pi +from .models import Pi, BOARD_TYPES from pibup.forms import UploadFileForm +BOARD_TYPE_LABELS = dict(BOARD_TYPES) + def home(request): - pis = Pi.objects.all() + pis = Pi.objects.all().order_by('board_type', 'port') + + board_groups = [] + for board_type, group in groupby(pis, key=lambda p: p.board_type): + board_groups.append({ + 'type': board_type, + 'label': BOARD_TYPE_LABELS.get(board_type, board_type), + 'pis': list(group), + }) return render(request, "index.html", { - 'pis': pis, + 'board_groups': board_groups, "domain_name": settings.DOMAIN_NAME, }) -def one(request, pino, template='fpga.html'): +def one(request, pino, template=None): # pino: Pi Number (the port on the network switch the Pi is plugged into.) - # template: the template to render (used to hack in the tt board page.) pi = get_object_or_404(Pi, port=pino) + # Auto-select template based on board type if not explicitly provided + if template is None: + if pi.board_type in ('tt_asic', 'tt_fpga'): + template = 'tt.html' + else: + template = 'fpga.html' + o=100+int(pino) ip=f'10.21.0.{o}' From bb4964fd64ba0c9923f02f968a6c8402bb9da949 Mon Sep 17 00:00:00 2001 From: Tim 'mithro' Ansell Date: Sun, 1 Mar 2026 11:20:52 +1100 Subject: [PATCH 2/3] Redesign templates with wafer.space-inspired CSS and responsive layout Rewrite pib.css as design system with CSS custom properties, CSS Grid, and Flexbox. Add base.html with shared header/footer and viewport meta tag (fixes mobile). Rewrite index.html with hero section and auto-fill card grid. Rewrite fpga.html and tt.html with toolbar, grid panels, and vanilla JS copyCode() replacing jQuery/f4pga dependencies. Style upload form button. Co-Authored-By: Claude Opus 4.6 --- .../pib/pibfpgas/src/pibfpgas/static/pib.css | 664 +++++++++++++++++- .../pibfpgas/src/pibfpgas/templates/base.html | 58 ++ .../pibfpgas/src/pibfpgas/templates/fpga.html | 343 ++++----- .../src/pibfpgas/templates/index.html | 132 ++-- .../pibfpgas/src/pibfpgas/templates/tt.html | 284 ++++---- .../pib/pibup/src/pibup/templates/upload.html | 4 +- 6 files changed, 1043 insertions(+), 442 deletions(-) create mode 100644 ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/base.html diff --git a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/static/pib.css b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/static/pib.css index 4d68063..f0e4f4a 100644 --- a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/static/pib.css +++ b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/static/pib.css @@ -1,40 +1,652 @@ - p { - margin: 1px; +/* ============================================ + fpgas.online Design System + Inspired by wafer.space aesthetic + ============================================ */ + +/* --- Custom Properties --- */ +:root { + --color-primary: #5CA7DB; + --color-primary-hover: #4a96ca; + --color-dark: #242930; + --color-dark-alt: #1E2228; + --color-bg: #F9FAFB; + --color-text: #606060; + --color-text-light: #999; + --color-white: #fff; + --color-border: #e5e7eb; + --color-danger: #e74c3c; + --color-success: #27ae60; + + --shadow-card: 0 5px 20px rgba(0, 0, 0, 0.04); + --shadow-card-hover: 0 8px 30px rgba(0, 0, 0, 0.08); + --radius: 4px; + --radius-lg: 8px; + + --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + --font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace; + + --space-xs: 0.25rem; + --space-sm: 0.5rem; + --space-md: 1rem; + --space-lg: 1.5rem; + --space-xl: 2rem; + --space-2xl: 3rem; +} + +/* --- Reset / Base --- */ +*, *::before, *::after { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: var(--font-body); + color: var(--color-text); + background: var(--color-bg); + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +a { + color: var(--color-primary); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +img { + max-width: 100%; + height: auto; +} + +/* --- Layout --- */ +.site-container { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.site-main { + flex: 1; +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 var(--space-lg); +} + +/* --- Header --- */ +.site-header { + background: var(--color-dark); + padding: var(--space-sm) 0; + position: sticky; + top: 0; + z-index: 100; +} + +.site-header .container { + display: flex; + align-items: center; + justify-content: space-between; +} + +.site-header__logo { + color: var(--color-white); + font-size: 1.25rem; + font-weight: 700; + text-decoration: none; +} + +.site-header__logo:hover { + text-decoration: none; + color: var(--color-primary); +} + +.site-nav { + display: flex; + gap: var(--space-lg); + list-style: none; + margin: 0; + padding: 0; +} + +.site-nav a { + color: #ccc; + font-size: 0.9rem; + transition: color 0.2s; +} + +.site-nav a:hover { + color: var(--color-white); + text-decoration: none; +} + +/* --- Hero --- */ +.hero { + background: var(--color-dark); + color: var(--color-white); + padding: var(--space-2xl) 0; +} + +.hero .container { + display: flex; + align-items: center; + gap: var(--space-2xl); +} + +.hero__content { + flex: 1; +} + +.hero__title { + font-size: 2rem; + font-weight: 700; + margin: 0 0 var(--space-md); + line-height: 1.2; +} + +.hero__text { + color: #b0b8c4; + font-size: 1.05rem; + margin: 0 0 var(--space-lg); + line-height: 1.7; +} + +.hero__actions { + display: flex; + gap: var(--space-md); + flex-wrap: wrap; +} + +.hero__image { + flex: 0 0 auto; +} + +.hero__image img { + max-height: 180px; + border-radius: var(--radius); +} + +/* --- Buttons --- */ +.btn { + display: inline-block; + padding: 0.5rem 1.25rem; + border-radius: var(--radius); + font-family: var(--font-body); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + border: 2px solid transparent; + transition: background 0.2s, border-color 0.2s, color 0.2s; + text-align: center; + line-height: 1.4; +} + +.btn:hover { + text-decoration: none; +} + +.btn--primary { + background: var(--color-primary); + color: var(--color-white); +} + +.btn--primary:hover { + background: var(--color-primary-hover); +} + +.btn--outline { + background: transparent; + border-color: rgba(255, 255, 255, 0.3); + color: var(--color-white); +} + +.btn--outline:hover { + border-color: var(--color-white); +} + +.btn--sm { + padding: 0.3rem 0.75rem; + font-size: 0.8rem; +} + +.btn--danger { + background: var(--color-danger); + color: var(--color-white); +} + +.btn--danger:hover { + background: #c0392b; +} + +/* Input buttons styled to match */ +input[type="submit"].btn, +input[type="button"].btn { + appearance: none; + -webkit-appearance: none; +} + +/* --- Section --- */ +.section { + padding: var(--space-2xl) 0; +} + +.section__title { + font-size: 1.5rem; + font-weight: 600; + color: var(--color-dark); + margin: 0 0 var(--space-lg); +} + +/* --- Board Cards (Index Page) --- */ +.board-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--space-lg); + margin-bottom: var(--space-2xl); +} + +.board-card { + background: var(--color-white); + border-radius: var(--radius); + box-shadow: var(--shadow-card); + overflow: hidden; + transition: box-shadow 0.2s, transform 0.2s; +} + +.board-card:hover { + box-shadow: var(--shadow-card-hover); + transform: translateY(-2px); +} + +.board-card__video { + position: relative; + background: #000; + aspect-ratio: 16 / 10; +} + +.board-card__video .video-js { + width: 100% !important; + height: 100% !important; + position: absolute; + top: 0; + left: 0; +} + +.board-card__body { + padding: var(--space-md); +} + +.board-card__badge { + display: inline-block; + background: var(--color-primary); + color: var(--color-white); + font-size: 0.7rem; + font-weight: 600; + padding: 0.15rem 0.5rem; + border-radius: 2px; + text-transform: uppercase; + letter-spacing: 0.03em; + margin-bottom: var(--space-sm); +} + +.board-card__name { + font-size: 1.1rem; + font-weight: 600; + color: var(--color-dark); + margin: 0 0 var(--space-sm); +} + +.board-card__link { + display: block; + text-align: center; + padding: 0.5rem; + background: var(--color-primary); + color: var(--color-white); + border-radius: var(--radius); + font-weight: 500; + font-size: 0.9rem; + transition: background 0.2s; +} + +.board-card__link:hover { + background: var(--color-primary-hover); + text-decoration: none; +} + +/* --- Detail Page --- */ +.detail-header { + background: var(--color-dark); + color: var(--color-white); + padding: var(--space-lg) 0; +} + +.detail-header__title { + font-size: 1.5rem; + font-weight: 700; + margin: 0; +} + +.detail-header__badge { + display: inline-block; + background: var(--color-primary); + color: var(--color-white); + font-size: 0.75rem; + font-weight: 600; + padding: 0.2rem 0.6rem; + border-radius: 2px; + text-transform: uppercase; + margin-left: var(--space-sm); + vertical-align: middle; +} + +/* --- Toolbar --- */ +.detail-toolbar { + background: var(--color-white); + border-bottom: 1px solid var(--color-border); + padding: var(--space-sm) 0; +} + +.detail-toolbar .container { + display: flex; + align-items: center; + gap: var(--space-lg); + flex-wrap: wrap; +} + +.toolbar-group { + display: flex; + align-items: center; + gap: var(--space-sm); + flex-wrap: wrap; +} + +.toolbar-group__label { + font-size: 0.8rem; + font-weight: 600; + color: var(--color-text-light); + text-transform: uppercase; + letter-spacing: 0.03em; + white-space: nowrap; +} + +.toolbar-btn { + display: inline-block; + padding: 0.25rem 0.6rem; + border-radius: var(--radius); + font-family: var(--font-body); + font-size: 0.8rem; + cursor: pointer; + border: 1px solid var(--color-border); + background: var(--color-white); + color: var(--color-text); + transition: background 0.15s, border-color 0.15s; +} + +.toolbar-btn:hover { + background: var(--color-bg); + border-color: var(--color-primary); +} + +/* --- Detail Panels (Video + Terminal) --- */ +.detail-panels { + display: grid; + grid-template-columns: 1fr 2fr; + gap: var(--space-md); + padding: var(--space-md) 0; +} + +.detail-panels--tt { + grid-template-columns: 1fr 4fr; +} + +.panel { + background: var(--color-white); + border-radius: var(--radius); + box-shadow: var(--shadow-card); + overflow: hidden; + min-height: 400px; +} + +.panel--video .video-js { + width: 100% !important; + height: 100% !important; +} + +.panel--terminal iframe { + width: 100%; + height: 100%; + border: none; + min-height: 400px; +} + +/* --- Status Bar --- */ +.status-bar { + background: var(--color-dark-alt); + padding: var(--space-md) 0; +} + +.status-bar .container { + display: flex; + align-items: flex-start; + gap: var(--space-md); + flex-wrap: wrap; +} + +.status-bar__log { + flex: 1; + min-width: 200px; +} + +.status-bar__log textarea { + width: 100%; + background: var(--color-dark); + color: #a8d8a8; + border: 1px solid #333; + border-radius: var(--radius); + padding: var(--space-sm); + font-family: var(--font-mono); + font-size: 0.8rem; + resize: vertical; +} + +.status-bar__controls { + display: flex; + align-items: center; + gap: var(--space-sm); + flex-wrap: wrap; +} + +.status-bar__controls input[type="text"] { + background: var(--color-dark); + color: var(--color-white); + border: 1px solid #444; + border-radius: var(--radius); + padding: 0.3rem 0.5rem; + font-family: var(--font-mono); + font-size: 0.8rem; +} + +/* --- Code Blocks (Direct Access) --- */ +.code-block { + position: relative; + background: var(--color-dark); + border-radius: var(--radius); + padding: var(--space-md); + margin-bottom: var(--space-md); +} + +.code-block pre { + margin: 0; + color: #a8d8a8; + font-family: var(--font-mono); + font-size: 0.85rem; + white-space: pre-wrap; + word-break: break-all; + padding-right: 4rem; +} + +.code-block__copy { + position: absolute; + top: var(--space-sm); + right: var(--space-sm); + background: rgba(255, 255, 255, 0.1); + color: #ccc; + border: 1px solid rgba(255, 255, 255, 0.15); + border-radius: var(--radius); + padding: 0.2rem 0.5rem; + font-size: 0.75rem; + cursor: pointer; + font-family: var(--font-body); + transition: background 0.15s, color 0.15s; +} + +.code-block__copy:hover { + background: rgba(255, 255, 255, 0.2); + color: var(--color-white); +} + +/* --- Links Grid --- */ +.links-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: var(--space-lg); +} + +.links-card { + background: var(--color-white); + border-radius: var(--radius); + box-shadow: var(--shadow-card); + padding: var(--space-lg); +} + +.links-card h3 { + font-size: 1rem; + font-weight: 600; + color: var(--color-dark); + margin: 0 0 var(--space-sm); +} + +.links-card ul { + list-style: none; + margin: 0; + padding: 0; +} + +.links-card li { + padding: var(--space-xs) 0; +} + +.links-card li a { + font-size: 0.9rem; +} + +/* --- Upload Form --- */ +.upload-form { + display: flex; + align-items: center; + gap: var(--space-sm); + flex-wrap: wrap; +} + +/* --- Footer --- */ +.site-footer { + background: var(--color-dark); + color: #888; + padding: var(--space-lg) 0; + margin-top: auto; + font-size: 0.85rem; +} + +.site-footer .container { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: var(--space-md); +} + +.site-footer a { + color: #aaa; +} + +.site-footer a:hover { + color: var(--color-white); +} + +.footer-links { + display: flex; + gap: var(--space-lg); + list-style: none; + margin: 0; + padding: 0; +} + +/* --- Responsive --- */ +@media (max-width: 768px) { + .hero .container { + flex-direction: column; + text-align: center; } - h1 { - padding: 5px; - margin: 0px; + .hero__actions { + justify-content: center; } - h2 { - padding: 1px; - margin: 0px; + + .hero__title { + font-size: 1.5rem; } - ul { - margin: 0px; + .detail-panels, + .detail-panels--tt { + grid-template-columns: 1fr; } - table.buttons { - width: fit-content; - float: left; - height: 100%; + .panel { + min-height: 300px; } - table.buttons td { - width: fit-content; + + .detail-toolbar .container { + flex-direction: column; + align-items: flex-start; } - table td { - padding: 5px; - border: 1px solid black; - text-align: center; + .site-header .container { + flex-direction: column; + gap: var(--space-sm); } - table { - width: 100%; - height: 90%; + + .site-nav { + gap: var(--space-md); + } + + .site-footer .container { + flex-direction: column; text-align: center; } - iframe { - width: 100%; - height: 100%; + + .footer-links { + justify-content: center; + } +} + +@media (max-width: 640px) { + .board-grid { + grid-template-columns: 1fr; + } + + .links-grid { + grid-template-columns: 1fr; + } + + .hero__image { + display: none; + } + + .status-bar .container { + flex-direction: column; } +} diff --git a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/base.html b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/base.html new file mode 100644 index 0000000..14f0300 --- /dev/null +++ b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/base.html @@ -0,0 +1,58 @@ +{% load static %} + + + + + + {% block title %}fpgas.online{% endblock %} + + + + + + + + + + + + {% block extra_head %}{% endblock %} + + + +
+ + + +
+ {% block content %}{% endblock %} +
+ + + +
+ +{% block extra_scripts %}{% endblock %} + + diff --git a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/fpga.html b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/fpga.html index 4d376a6..c0bc7dc 100644 --- a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/fpga.html +++ b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/fpga.html @@ -1,198 +1,153 @@ - - - - - - - - - - - - - - - - - - - - Home - -
Edit this page on GitHub.
- - - - - - - - - - - - - - - - -
-

Accessing pi{{pi.port}}

-
-{% include "upload.html" %} -
- - - - - -
- Turn it off and on again: - - - - -
- - - - - - -

Demos:

- - - - -
-
- - - -
-
-
- - - +{% extends "base.html" %} +{% load static %} + +{% block title %}pi{{ pi.port }} - fpgas.online{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + +
+
+

+ pi{{ pi.port }} + {{ pi.get_board_type_display }} +

+
+
+ +
+
+
+ Controls + + + + + +
+
+ Demos + + + + +
+
+ Upload + {% include "upload.html" %} +
+
+
+ +
+
+
+
+
- - Pi patch cable color: {{pi.loc}} {{pi.cable_color}}
- -

Accessing directly

- -
-
-
vlc https://{{ domain_name }}/live/pi{{pi.port}}.m3u8
-
+
+
- -
- Use your own ssh client. user: pi, host: {{ domain_name }}, port {{o}}22, - (password is in login banner)
- click to copy: -
-
-
ssh -p {{o}}22 pi@{{ domain_name }}
-
+
+ + + +
+
+
+ +
+
+ + + +
+
+
+ +
+
+

Direct Access

+ +

Pi patch cable color: {{ pi.location }} {{ pi.cable_color }}

+ +
+
vlc https://{{ domain_name }}/live/pi{{ pi.port }}.m3u8
+ +
+ +

Use your own SSH client. User: pi, host: {{ domain_name }}, port {{ o }}22 (password is in login banner):

+ +
+
ssh -p {{ o }}22 pi@{{ domain_name }}
+ +
+ +
+
scp -P {{ o }}22 * pi@{{ domain_name }}:Uploads
+ +
+
+
+ +
+
+

Useful Links

+
- - - - - - -

Useful links

- - - - + + + + + + + + + +{% endblock %} + +{% block extra_scripts %} + + + +{% endblock %} diff --git a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/index.html b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/index.html index 484e8b8..8b5d49d 100644 --- a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/index.html +++ b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/index.html @@ -1,82 +1,68 @@ - - +{% extends "base.html" %} +{% load static %} - +{% block title %}fpgas.online - Access an FPGA right now!{% endblock %} - - +{% block content %} - + {% for group in board_groups %} +

{{ group.label }}

+
- + {% for pi in group.pis %} +
+
+ +
+
+ {{ group.label }} +

pi{{ pi.port }}

+ Use this board +
+
+ {% endfor %} - +
+ {% endfor %} -
Edit this pageon GitHub.
+ + - - - - - - -

Access an FPGA right now!

-

On this page anyone (including you!) can remotely access and control an FPGA development board! There is even a camera to see the LEDs.

- - - - - - -
Get Started - Behind the scenesHelp out!
-

There are ~10 FPGAs boards connected to this system, so there should always be one free and ready for you to use!

-
- -
- -
- -{% for pi in pis %} - - - - - - - -

FPGA pi{{pi.port}}

Use this FPGA
- -
- -{% endfor %} - -
- - - +{% endblock %} diff --git a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/tt.html b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/tt.html index d8daaae..80b2a98 100644 --- a/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/tt.html +++ b/ansible/roles/site/files/pib/pibfpgas/src/pibfpgas/templates/tt.html @@ -1,149 +1,139 @@ - - - - - - - - - - - - - - - - - - Home - -
Edit this page on GitHub.
- - - - - - - - - - - - - - - - - - - - - -
-

Accessing pi{{pi.port}}

-
- - - - - - -

Pi Controls:

- Turn it off and on again: -
- - - - - -

Demos:

- -
-
- - - -
-
-
+{% extends "base.html" %} +{% load static %} + +{% block title %}pi{{ pi.port }} - Tiny Tapeout - fpgas.online{% endblock %} + +{% block extra_head %} + +{% endblock %} + +{% block content %} + +
+
+

+ pi{{ pi.port }} + {{ pi.get_board_type_display }} +

+
+
+ +
+
+
+ Controls + +
+
+ Demos + +
+
+ USB Power + + +
+
+
+ +
+
+
+
+
- - -
- Use your own ssh client
user: pi
host: {{ domain_name }}
port {{o}}22
- like this (click to copy): - -
-
-
ssh -p {{o}}22 pi@{{ domain_name }}
-
+
+
- -
-
- -

Page Controls:

- - Send messages to the debug window: - - -

- - - - - - - - - - - - - - - Pi patch cable color: {{pi.loc}} {{pi.cable_color}}
- - - - - - - - - - - - - -

Useful links

- -

Tiny Tapeout

-
    -
  • Tiny Tapeout is an educational project that makes it easier and cheaper than ever to get your designs manufactured on a real chip!
  • -
  • 910 : KianV uLinux SoC The Tiny Tapeout project featured here. -
- -

Host:

- - - - + + + + +
+
+
+ +
+
+ + + + + + +
+
+
+ +
+
+

Direct Access

+ +

Pi patch cable color: {{ pi.location }} {{ pi.cable_color }}

+ +

Use your own SSH client. User: pi, host: {{ domain_name }}, port {{ o }}22 (password is in login banner):

+ +
+
ssh -p {{ o }}22 pi@{{ domain_name }}
+ +
+
+
+ +
+
+

Useful Links

+ +
+
+ + + + + + + +{% endblock %} + +{% block extra_scripts %} + + + +{% endblock %} diff --git a/ansible/roles/site/files/pib/pibup/src/pibup/templates/upload.html b/ansible/roles/site/files/pib/pibup/src/pibup/templates/upload.html index 3172993..d9268bd 100644 --- a/ansible/roles/site/files/pib/pibup/src/pibup/templates/upload.html +++ b/ansible/roles/site/files/pib/pibup/src/pibup/templates/upload.html @@ -1,5 +1,5 @@ -
+ {% csrf_token %} {{form}} - +
From 5d4cba895a25c38f74a15f9673cca818ed85701c Mon Sep 17 00:00:00 2001 From: Tim 'mithro' Ansell Date: Sun, 1 Mar 2026 11:21:10 +1100 Subject: [PATCH 3/3] Add Makefile for local dev setup and server Provides make serve (full setup + launch), make setup, make migrate, make fixtures, and make clean. Uses uv for venv and pip, generates local_settings.py with InMemoryChannelLayer (no Redis needed). Co-Authored-By: Claude Opus 4.6 --- ansible/roles/site/files/pib/Makefile | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 ansible/roles/site/files/pib/Makefile diff --git a/ansible/roles/site/files/pib/Makefile b/ansible/roles/site/files/pib/Makefile new file mode 100644 index 0000000..3e3689b --- /dev/null +++ b/ansible/roles/site/files/pib/Makefile @@ -0,0 +1,54 @@ +VENV := .venv +UV := uv +PYTHON := $(UV) run python +MANAGE := $(PYTHON) manage.py +PORT := 8000 + +APPS := -e pibfpgas -e pibup -e pistat -e snmp_switch +DEPS := 'channels[daphne]' channels_redis + +FIXTURE ?= pibfpgas/fixtures/fpgas.online.json + +.PHONY: serve setup migrate fixtures clean help + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## ' $(MAKEFILE_LIST) | \ + awk 'BEGIN {FS = ":.*?## "}; {printf " make %-12s %s\n", $$1, $$2}' + +serve: setup ## Start the local dev server (default: port 8000) + $(MANAGE) runserver 0.0.0.0:$(PORT) + +setup: $(VENV)/pyvenv.cfg pib/local_settings.py | migrate fixtures ## Full setup: venv, deps, settings, migrate, fixtures + +$(VENV)/pyvenv.cfg: + $(UV) venv $(VENV) + $(UV) pip install $(APPS) $(DEPS) + +pib/local_settings.py: + @echo "Creating pib/local_settings.py for local dev..." + @printf '%s\n' \ + '# Local development settings' \ + '# Auto-generated by Makefile. Edit freely; .gitignored.' \ + '' \ + "ALLOWED_HOSTS = ['*']" \ + '' \ + "DOMAIN_NAME = 'localhost:$(PORT)'" \ + "PI_PW = 'dev-password'" \ + '' \ + '# In-memory channel layer (no Redis needed for local dev)' \ + 'CHANNEL_LAYERS = {' \ + ' "default": {' \ + ' "BACKEND": "channels.layers.InMemoryChannelLayer",' \ + ' },' \ + '}' > pib/local_settings.py + +migrate: $(VENV)/pyvenv.cfg ## Run Django migrations + $(MANAGE) migrate + +fixtures: $(VENV)/pyvenv.cfg ## Load fixture data (override: FIXTURE=path/to/file.json) + $(MANAGE) loaddata $(FIXTURE) + +clean: ## Remove venv, database, and local_settings.py + rm -rf $(VENV) + rm -f db.sqlite3 + rm -f pib/local_settings.py