Skip to content
Open
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
161 changes: 161 additions & 0 deletions HTMLTemplates/certifications_v2.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
<%def name="scripts()">
<script>
// Pagination: show one page at a time with a 15s timer progress bar
const PAGE_SECONDS = 10;
let pageIndex = 0;
let pageTimer = null;
let progressTimer = null;

// Data from server (pre-serialized JSON)
function getToolsData(){
const el = document.getElementById('tools-json');
if(!el) return [];
try { return JSON.parse(el.textContent || '[]'); } catch(e){ return []; }
}
let toolsData = [];

function renderPage(idx){
const container = document.getElementById('cards-grid');
if(!container) return;
const pageSize = 5; // exactly 5 tools per page
const start = (idx * pageSize) % toolsData.length;
const slice = toolsData.slice(start, start + pageSize);
const items = slice.length < pageSize ? slice.concat(toolsData.slice(0, pageSize - slice.length)) : slice;
var html = '';
items.forEach(function(tool){
var sources = '';
if (tool.image_url) { sources += '<source srcset="' + tool.image_url + '" type="image/avif"/>'; }
if (tool.img_avif) { sources += '<source srcset="' + tool.img_avif + '" type="image/avif"/>'; }
if (tool.img_png) { sources += '<source srcset="' + tool.img_png + '" type="image/png"/>'; }
if (tool.img_jpg) { sources += '<source srcset="' + tool.img_jpg + '" type="image/jpeg"/>'; }
var imgSrc = tool.img_png || tool.img_jpg || tool.image_url || '/static/tools/placeholder.svg';
var membersHtml = '';
if (!tool.members || tool.members.length === 0) {
membersHtml = "<div class='member' style='color:var(--muted)'>No certified members</div>";
} else {
for (var i=0;i<tool.members.length;i++){
var m = tool.members[i];
membersHtml += "<div class='member level-" + m.level + "'>" +
"<div class='name'>" + m.displayName + "</div>" +
"<div class='level-chip level-" + m.level + "'>" + m.level_name + "</div>" +
"</div>";
}
}
html += '<div class="card">' +
'<div class="card-top">' +
'<picture class="tool-hero">' + sources +
'<img class="tool-hero" src="' + imgSrc + '" alt="' + tool.name + '" onerror="this.onerror=null; this.src=\'/static/tools/placeholder.svg\';"/>' +
'</picture>' +
'<div class="card-header">' +
'<div class="card-title">' + tool.name + '</div>' +
'</div>' +
'</div>' +
'<div class="card-body">' +
membersHtml +
'</div>' +
'</div>';
});
container.innerHTML = html;
}

function startPagination(){
const bar = document.getElementById('progress-bar');
const inner = document.getElementById('progress-inner');
// Load tools data now that DOM is ready
toolsData = getToolsData();
function tick(){
// advance page
pageIndex = (pageIndex + 1) % Math.ceil(toolsData.length / 5);
renderPage(pageIndex);
// restart progress
inner.style.transition = 'none';
inner.style.width = '0%';
requestAnimationFrame(function(){
requestAnimationFrame(function(){
inner.style.transition = 'width ' + PAGE_SECONDS + 's linear';
inner.style.width = '100%';
});
});
}
// initial render and animate
renderPage(pageIndex);
inner.style.transition = 'width ' + PAGE_SECONDS + 's linear';
inner.style.width = '100%';
if(pageTimer) clearInterval(pageTimer);
pageTimer = setInterval(tick, PAGE_SECONDS * 1000);
}

window.addEventListener('load', startPagination);

// Live clock in header (right side)
function updateClock(){
const el = document.getElementById('cm-clock');
if(!el) return;
const now = new Date();
el.textContent = now.toLocaleString();
}
setInterval(updateClock, 1000);
window.addEventListener('load', updateClock);
</script>
</%def>
<%def name="head()">
<style>
:root{ --c10:#0e7490; --c20:#166534; --c30:#92400e; --c40:#7f1d1d; --card:#0b1220; --text:#e5e7eb; --muted:#94a3b8; --bg:#0a0f1a; --border:#1f2937; --accent:#60a5fa; --basic:#ef4444; --certified:#22c55e; --dof:#eab308; --instructor:#3b82f6; --certifier:#9ca3af; }
body{ font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; background: var(--bg); color: var(--text); }
/* Member row accents per level */
.member.level-1{ background: rgba(239,68,68,.12); border-left:4px solid var(--basic); }
.member.level-10{ background: rgba(34,197,94,.12); border-left:4px solid var(--certified); }
.member.level-20{ background: rgba(234,179,8,.12); border-left:4px solid var(--dof); }
.member.level-30{ background: rgba(59,130,246,.12); border-left:4px solid var(--instructor); }
.member.level-40{ background: rgba(156,163,175,.12); border-left:4px solid var(--certifier); }
.grid{ display:grid; grid-template-columns: repeat(1, minmax(0, 1fr)); gap:16px; }
@media(min-width:640px){ .grid{ grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media(min-width:900px){ .grid{ grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media(min-width:1200px){ .grid{ grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media(min-width:1440px){ .grid{ grid-template-columns: repeat(5, minmax(0, 1fr)); } }
.card{ background:var(--card); border-radius:12px; box-shadow: 0 2px 12px rgba(0,0,0,.4); border:1px solid var(--border); overflow:hidden; display:flex; flex-direction:column; }
.card-top{ position:sticky; top:0; z-index:1; background: var(--card); border-bottom:1px solid var(--border); }
.card-header{ display:flex; align-items:center; justify-content:center; gap:12px; padding:12px 14px; }
.card-title{ font-weight:700; color:var(--text); font-size:24px; text-align:center; width:100%; }
.tool-hero{ width:100%; height:200px; object-fit:contain; background:#0b1220; border-bottom:1px solid var(--border); }
.card-body{ padding:10px 14px; display:flex; flex-direction:column; gap:8px; }
.member{ display:flex; align-items:center; justify-content:space-between; padding:6px 8px; border-radius:8px; }
.name{ font-size:14px; color:var(--text); font-weight:500; }
.level-chip{ font-size:12px; padding:2px 8px; border-radius:9999px; border:1px solid var(--border); color:#0a0a0a; background:#e5e7eb; }
/* Chips per level */
.level-chip.level-1{ background: var(--basic); }
.level-chip.level-10{ background: var(--certified); }
.level-chip.level-20{ background: var(--dof); }
.level-chip.level-30{ background: var(--instructor); }
.level-chip.level-40{ background: var(--certifier); }
#cards-container{ max-height: calc(100vh - 160px); overflow:hidden; padding-right:6px; }
.header{ display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin: 6px 0 10px; }
.masthead{ display:grid; grid-template-columns: 1fr auto 1fr; align-items:center; gap:12px; margin: 6px 0 6px; }
.masthead .left{ justify-self:start; }
.masthead .center{ justify-self:center; }
.masthead .right{ justify-self:end; }
.title{ font-size:40px; font-weight:700; color: var(--text); }
.clock{ color: var(--muted); font-size: 28px; }
.progress { height: 6px; width: 100%; background: #111827; border-radius: 9999px; overflow: hidden; border:1px solid var(--border); }
.progress > .bar { height: 100%; width: 0%; background: var(--accent); }
.logo-bar{ display:flex; align-items:center; justify-content:center; padding:0; }
.logo-bar img{ max-height:80px; width:auto; opacity:.9; }
</style>
</%def>

<%def name="title()">Certification Monitor</%def>
<%inherit file="base.mako"/>

<div class="masthead">
<div class="left title">Certification Monitor</div>
<div class="center logo-bar">
<img src="https://photos.smugmug.com/Logos/TFD-TFI-TUF-Logos/i-dDKzxPw/0/NcCVLCWrFZQczNXLvBsB4qgHDSC7bxX4P8mSH7DS3/XL/TFI%20logo%20BLACK%20high%20quality%20no%20tagline-XL.jpg" alt="TFI logo" />
</div>
<div class="right clock" id="cm-clock"></div>
</div>

<div id="cards-container">
<script id="tools-json" type="application/json">${ (tools_json if tools_json is not UNDEFINED else '[]') | n }</script>
<div class="progress" id="progress-bar"><div class="bar" id="progress-inner"></div></div>
<div class="grid" id="cards-grid"></div>
</div>
14 changes: 6 additions & 8 deletions HTMLTemplates/links.mako
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ ${self.logo()}<br/>
<fieldset><legend>Personal</legend>
<UL>
% if inBuilding:
<LI><A HREF="/station/checkout?barcode=${barcode}">Check out of BFF</A>
<LI><A HREF="/station/checkout?barcode=${barcode}">Check out of The Forge</A>
% else:
<LI><A HREF="/station/checkin?barcode=${barcode}">Check into BFF</A>
<LI><A HREF="/station/checkin?barcode=${barcode}">Check into The Forge</A>
% endif
<LI><A HREF="/certifications/user?barcode=${barcode}">My Shop Certifications</A>
% if role.value != 0:
Expand All @@ -37,7 +37,7 @@ ${self.logo()}<br/>
</fieldset><br/>
<fieldset><legend>General</legend>
<UL>
<LI><A HREF="/whoishere">See who is at BFF</A>
<LI><A HREF="/whoishere">See who is at The Forge</A>
<LI><A HREF="https://calendar.google.com/calendar/embed?src=h75eigkfjvngvpff1dq0af74mk%40group.calendar.google.com&ctz=America%2FNew_York">TFI Calendar</A>
<LI><A HREF="https://app.theforgeinitiative.org/">Forge Member App</A>
</UL>
Expand All @@ -47,9 +47,7 @@ ${self.logo()}<br/>
<fieldset>
<legend>Keyholder</legend>
<UL>
<LI><A HREF="http://192.168.1.10">Downstairs Door (Works ONLY when at BFF)</A></LI>
<LI><A HREF="http://10.0.0.10">Upstairs Door (Works ONLY when at BFF)</A></LI>

<LI><A HREF="http://192.168.1.10">Front Door (Works ONLY when at The Forge)</A></LI>
<LI><A HREF="/station/makeKeyholder?barcode=${barcode}">Make ME Keyholder</A></LI>
<LI><A HREF="/station/updatePresent">Update who is in building</A></LI>
<LI><A HREF="/admin/oops">Oops (Didn't meant to close building)</A></LI>
Expand Down Expand Up @@ -88,7 +86,7 @@ ${self.logo()}<br/>
</fieldset><br/>
% endif
% endif
<fieldset><legend>BFF Stations</legend>
<fieldset><legend>Check-in Stations</legend>
<UL>
<LI><A HREF="/station">Main Station</A></LI>
<LI><A HREF="/guests">Guest Station</A></LI>
Expand All @@ -97,4 +95,4 @@ ${self.logo()}<br/>
</fieldset></br/>
<hr/>
To add feature requests or report issues, please go to:<A HREF="https://github.com/alan412/CheckMeIn/issues">https://github.com/alan412/CheckMeIn/issues</A>
<br/>
<br/>
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
a system for checking into and out of a building

# Setup
Start in the repo. e.g. ```cd .../CheckMeIn``` for wherever you clone the repo.
You'll need a python venv, set it up like this:
1. ```python3 -m venv venv```
2. ```source venv/bin/activate```
Expand All @@ -23,13 +24,13 @@ DO NOT push these renamed files to the origin repository.
Once you are satisfied that you have the dependencies met, and the unit tests are passing, then to run the
server, you will execute:

```python3 checkmein.py development.conf```
```python3 checkMeIn.py development.conf```

You can connect to your server using a local browser at "http://localhost:8089"
Note:
* When first starting, assuming you ran the tests, you may choose to
```mkdir data```
```cp testData test.db data/checkmein.db```
* When first starting, assuming you ran the tests, you may choose to
```mkdir data```
```cp testData/test.db data/checkMeIn.db```
This gives you a database with a couple of members and an admin user whose name
is 'admin' and password is 'password'.

Expand Down
4 changes: 3 additions & 1 deletion checkMeIn.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from mako.lookup import TemplateLookup
import cherrypy
import cherrypy.process.plugins
import os

import engine
from webBase import WebBase, Cookie
Expand All @@ -24,8 +25,9 @@ def update(self, msg):
cherrypy.engine.publish(self.updateChannel, fullMessage)

def __init__(self):
templates_dir = os.path.join(os.path.dirname(__file__), 'HTMLTemplates')
self.lookup = TemplateLookup(
directories=['HTMLTemplates'], default_filters=['h'])
directories=[templates_dir], default_filters=['h'], filesystem_checks=True)
self.updateChannel = 'updates'
self.engine = engine.Engine(
cherrypy.config["database.path"], cherrypy.config["database.name"], self.update)
Expand Down
2 changes: 1 addition & 1 deletion docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def getDocumentation():
returns="Returns a webpage",
notes=["This shows a list of links that barcode might find useful based off their role",
"If barcode is left off, it is a list of links for display stations"]),
Doc('Unlock', '/unlock?location=BFF&barcode=<barcode>',
Doc('Unlock', '/unlock?location=TFI&barcode=<barcode>',
returns="Returns the station webpage",
notes=["This records the door was unlocked and checks the person that unlocks it in. For use of door app ONLY"]),
Doc('Get Keyholder list', '/admin/getKeyholderJSON',
Expand Down
4 changes: 4 additions & 0 deletions etc/Caddyfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
checkmein.stage.theforgeinitiative.org {
tls
reverse_proxy localhost:8447
}
20 changes: 20 additions & 0 deletions etc/caddy.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/local/bin/caddy run --environ --config /opt/caddy/Caddyfile
ExecReload=/usr/local/bin/caddy reload --config /opt/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
15 changes: 15 additions & 0 deletions etc/checkmein.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[Unit]
Description=CheckMeIn
After=network-online.target
Requires=network-online.target

[Service]
User=checkmein
Restart=on-failure
RestartSec=30
AmbientCapabilities=CAP_NET_BIND_SERVICE
WorkingDirectory=/opt/checkmein/src
ExecStart=/opt/checkmein/venv/bin/python3 checkMeIn.py production.conf

[Install]
WantedBy=multi-user.target
3 changes: 0 additions & 3 deletions production.conf
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
[global]
log.error_file : os.path.join(os.getcwd(), 'checkMeIn.log')
log.access_file : ''
log.screen : False
server.socket_host : '127.0.0.1'
server.socket_port : 8447
database.path : 'data/'
Expand Down
Binary file added static/tools/3dprinter.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/Stretcher shrinker.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/blind rivet gun.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/cnc.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/dremel.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/drill.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/drillpress.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/grinder.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions static/tools/images.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"1": "/static/tools/sheet%20metal%20brake.webp",
"2": "/static/tools/blind%20rivet%20gun.webp",
"3": "/static/tools/Stretcher%20shrinker.webp",
"4": "/static/tools/3dprinter.webp",
"5": "/static/tools/drill.webp",
"6": "/static/tools/solderingiron.webp",
"7": "/static/tools/dremel.webp",
"8": "/static/tools/8.svg",
"9": "/static/tools/drillpress.webp",
"10": "/static/tools/10.svg",
"11": "/static/tools/11.svg",
"12": "/static/tools/tablemountedjigsaw.webp",
"13": "/static/tools/13.svg",
"14": "/static/tools/14.svg",
"15": "/static/tools/cnc.webp",
"16": "/static/tools/lathe.webp",
"17": "/static/tools/tablesaw.webp",
"18": "/static/tools/miter.webp",
"19": "/static/tools/grinder.webp"
}
Binary file added static/tools/lathe.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/miter.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/scrollsaw.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/sheet metal brake.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/solderingiron.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/tablemountedjigsaw.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added static/tools/tablesaw.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion tests/misc_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ def test_metrics(self):

def test_unlock(self):
with self.patch_session():
self.getPage("/unlock?location=BFF&barcode=100091")
self.getPage("/unlock?location=TFI&barcode=100091")
self.assertStatus('303 See Other')
Loading