security hardening + drafts/attachments
This commit is contained in:
1284
web/src/index.js
Normal file
1284
web/src/index.js
Normal file
File diff suppressed because it is too large
Load Diff
45
web/src/views/account_password.ejs
Normal file
45
web/src/views/account_password.ejs
Normal file
@@ -0,0 +1,45 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Change Password</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:700px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
label{display:block;margin:10px 0 6px;color:#a9b7c6}
|
||||
input{width:100%;padding:12px;border-radius:10px;border:1px solid #233043;background:#0e1520;color:#e8eef7}
|
||||
button{padding:12px 14px;border-radius:10px;border:1px solid #233043;background:#1c2a3d;color:#e8eef7;margin-top:12px;width:100%}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6;font-size:13px;line-height:1.4}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700">Change Password</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<p class="muted">This changes your <b>local</b> password (SSO passwords are managed by Google/Microsoft). If you don’t have a local identity yet, this will create one.</p>
|
||||
<form method="post" action="/account/password">
|
||||
<label>Current password (leave blank if you have no local password yet)</label>
|
||||
<input name="currentPassword" type="password" />
|
||||
<label>New password</label>
|
||||
<input name="newPassword" type="password" required />
|
||||
<label>Confirm new password</label>
|
||||
<input name="confirmPassword" type="password" required />
|
||||
<button type="submit">Update password</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
64
web/src/views/admin_audit.ejs
Normal file
64
web/src/views/admin_audit.ejs
Normal file
@@ -0,0 +1,64 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Audit Log</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1200px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{border-bottom:1px solid #233043;padding:10px 6px;text-align:left;color:#a9b7c6;font-size:13px;vertical-align:top}
|
||||
th{color:#e8eef7}
|
||||
code{font-family:ui-monospace,Menlo,Consolas,monospace;color:#d6e3f3}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700">Audit Log</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/admin/users">Users</a>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<div class="muted">Showing latest <%= logs.length %> events (most recent first).</div>
|
||||
<div style="overflow:auto;margin-top:10px">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Time</th>
|
||||
<th>Actor</th>
|
||||
<th>Action</th>
|
||||
<th>Target</th>
|
||||
<th>IP</th>
|
||||
<th>Metadata</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% logs.forEach(l => { %>
|
||||
<tr>
|
||||
<td><%= l.created_at %></td>
|
||||
<td><%= l.actor_email || '' %></td>
|
||||
<td><code><%= l.action %></code></td>
|
||||
<td><%= l.target_type || '' %> <%= l.target_id || '' %></td>
|
||||
<td><%= l.ip || '' %></td>
|
||||
<td><code><%= JSON.stringify(l.metadata || {}) %></code></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
119
web/src/views/admin_users.ejs
Normal file
119
web/src/views/admin_users.ejs
Normal file
@@ -0,0 +1,119 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Users</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1100px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{border-bottom:1px solid #233043;padding:10px 6px;text-align:left;color:#a9b7c6;font-size:13px;vertical-align:top}
|
||||
th{color:#e8eef7}
|
||||
input,select{padding:10px;border-radius:10px;border:1px solid #233043;background:#0e1520;color:#e8eef7;width:100%}
|
||||
button{padding:10px 12px;border-radius:10px;border:1px solid #233043;background:#1c2a3d;color:#e8eef7}
|
||||
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}
|
||||
.col6{grid-column:span 12}
|
||||
@media(min-width:900px){.col6{grid-column:span 6}}
|
||||
.pill{display:inline-block;padding:2px 8px;border:1px solid #233043;border-radius:999px;font-size:12px;color:#a9b7c6}
|
||||
.danger{background:#2b1216;border-color:#5a1e28}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700">User Management</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Create local user</h2>
|
||||
<form method="post" action="/admin/users/create" class="grid">
|
||||
<div class="col6">
|
||||
<label class="muted">Email</label>
|
||||
<input name="email" placeholder="user@company.com" required />
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">Display name</label>
|
||||
<input name="displayName" placeholder="Jane Doe" />
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">Role</label>
|
||||
<select name="role">
|
||||
<option value="apm">apm</option>
|
||||
<option value="pm">pm</option>
|
||||
<option value="viewer">viewer</option>
|
||||
<option value="owner">owner</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">Temporary password</label>
|
||||
<input name="tempPassword" placeholder="Temp password" required />
|
||||
</div>
|
||||
<div style="grid-column:span 12">
|
||||
<button type="submit">Create user</button>
|
||||
</div>
|
||||
</form>
|
||||
<p class="muted" style="margin-top:10px">SSO users (Google/Microsoft) will appear here after their first login.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Users</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Name</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Identities</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% users.forEach(u => { %>
|
||||
<tr>
|
||||
<td><%= u.email || '(no email)' %></td>
|
||||
<td><%= u.display_name || '' %></td>
|
||||
<td><span class="pill"><%= u.role %></span></td>
|
||||
<td>
|
||||
<% if (u.disabled) { %>
|
||||
<span class="pill danger">disabled</span>
|
||||
<% } else { %>
|
||||
<span class="pill">active</span>
|
||||
<% } %>
|
||||
</td>
|
||||
<td class="muted">
|
||||
<%= (u.providers || '').split(',').filter(Boolean).join(', ') %>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="/admin/users/<%= u.id %>/reset" style="display:inline-block;min-width:220px">
|
||||
<input name="newPassword" placeholder="New temp password" />
|
||||
<button type="submit" style="margin-top:6px;width:100%">Reset local password</button>
|
||||
</form>
|
||||
<div style="height:8px"></div>
|
||||
<form method="post" action="/admin/users/<%= u.id %>/toggle" style="display:inline-block;width:220px">
|
||||
<button type="submit" style="width:100%"><%= u.disabled ? 'Enable user' : 'Disable user' %></button>
|
||||
</form>
|
||||
<div style="height:8px"></div>
|
||||
<form method="post" action="/admin/users/<%= u.id %>/delete" onsubmit="return confirm('Delete user? This cannot be undone.');" style="display:inline-block;width:220px">
|
||||
<button type="submit" style="width:100%;background:#2b1216;border-color:#5a1e28">Delete user</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
59
web/src/views/docs.ejs
Normal file
59
web/src/views/docs.ejs
Normal file
@@ -0,0 +1,59 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Docs</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1100px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}
|
||||
.nav{grid-column:span 12}
|
||||
.content{grid-column:span 12}
|
||||
@media(min-width:900px){.nav{grid-column:span 3}.content{grid-column:span 9}}
|
||||
.nav a{display:block;padding:10px;border:1px solid #233043;border-radius:10px;margin-bottom:8px;background:#0e1520}
|
||||
.doc{background:#0e1520;border:1px solid #233043;border-radius:10px;padding:14px;overflow:auto}
|
||||
.doc h1,.doc h2,.doc h3{color:#e8eef7}
|
||||
.doc p,.doc li{color:#a9b7c6;line-height:1.45}
|
||||
.doc code,.doc pre{font-family:ui-monospace,Menlo,Consolas,monospace}
|
||||
.doc pre{background:#0b0f14;border:1px solid #233043;border-radius:10px;padding:12px;color:#d6e3f3;overflow:auto}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700">Docs</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="grid">
|
||||
<div class="nav">
|
||||
<div class="card">
|
||||
<% docs.forEach(d => { %>
|
||||
<a href="/docs/<%= d.slug %>"><%= d.title %></a>
|
||||
<% }) %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="card">
|
||||
<div class="muted">Viewing: <b><%= current.title %></b></div>
|
||||
<div class="muted">Source file: <code><%= current.path %></code></div>
|
||||
</div>
|
||||
<div class="doc">
|
||||
<%- html %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
75
web/src/views/draft_edit.ejs
Normal file
75
web/src/views/draft_edit.ejs
Normal file
@@ -0,0 +1,75 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Draft</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1100px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
textarea,input,select{width:100%;padding:12px;border-radius:10px;border:1px solid #233043;background:#0e1520;color:#e8eef7}
|
||||
button{padding:12px 14px;border-radius:10px;border:1px solid #233043;background:#1c2a3d;color:#e8eef7}
|
||||
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}
|
||||
.col6{grid-column:span 12}
|
||||
@media(min-width:900px){.col6{grid-column:span 6}}
|
||||
pre{white-space:pre-wrap;background:#0e1520;border:1px solid #233043;border-radius:10px;padding:12px;color:#d6e3f3}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700"><%= kind.toUpperCase() %> Draft</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="card">
|
||||
<div class="muted"><b>Project:</b> <%= project ? project.name : '(unassigned)' %></div>
|
||||
<div class="muted"><b>Source email:</b> <% if (email) { %><a href="/inbox/<%= email.id %>"><%= email.subject || email.id %></a><% } else { %>—<% } %></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;justify-content:flex-end;margin-bottom:10px">
|
||||
<a href="/drafts/<%= kind %>/<%= draft.id %>/export.md">Download .md</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/drafts/<%= kind %>/<%= draft.id %>/save" class="grid">
|
||||
<div class="col6">
|
||||
<label class="muted">Title</label>
|
||||
<input name="title" value="<%= draft.title || '' %>" />
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">Status</label>
|
||||
<select name="status">
|
||||
<% ['draft','ready','sent'].forEach(s => { %>
|
||||
<option value="<%= s %>" <%= draft.status===s?'selected':'' %>><%= s %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
<div style="grid-column:span 12">
|
||||
<label class="muted">Body (Markdown)</label>
|
||||
<textarea name="body" rows="14"><%= draft.body || '' %></textarea>
|
||||
</div>
|
||||
<div style="grid-column:span 12">
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Preview</h2>
|
||||
<div class="muted" style="margin-bottom:8px">Rendered Markdown preview</div>
|
||||
<div style="background:#0e1520;border:1px solid #233043;border-radius:10px;padding:12px;overflow:auto">
|
||||
<%- previewHtml %>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
70
web/src/views/drafts_list.ejs
Normal file
70
web/src/views/drafts_list.ejs
Normal file
@@ -0,0 +1,70 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Drafts</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1200px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{border-bottom:1px solid #233043;padding:10px 6px;text-align:left;color:#a9b7c6;font-size:13px;vertical-align:top}
|
||||
th{color:#e8eef7}
|
||||
.pill{display:inline-block;padding:2px 8px;border:1px solid #233043;border-radius:999px;font-size:12px;color:#a9b7c6}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700">Drafts</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="card">
|
||||
<div class="muted">PCO and RFI drafts generated from emails. Nothing is sent automatically.</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">PCO drafts</h2>
|
||||
<table>
|
||||
<thead><tr><th>Created</th><th>Project</th><th>Title</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
<% pcos.forEach(d => { %>
|
||||
<tr>
|
||||
<td><%= d.created_at %></td>
|
||||
<td><%= d.project_name || '' %></td>
|
||||
<td><a href="/drafts/pco/<%= d.id %>"><%= d.title || '(untitled)' %></a></td>
|
||||
<td><span class="pill"><%= d.status %></span></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">RFI drafts</h2>
|
||||
<table>
|
||||
<thead><tr><th>Created</th><th>Project</th><th>Title</th><th>Status</th></tr></thead>
|
||||
<tbody>
|
||||
<% rfis.forEach(d => { %>
|
||||
<tr>
|
||||
<td><%= d.created_at %></td>
|
||||
<td><%= d.project_name || '' %></td>
|
||||
<td><a href="/drafts/rfi/<%= d.id %>"><%= d.title || '(untitled)' %></a></td>
|
||||
<td><span class="pill"><%= d.status %></span></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
110
web/src/views/email_rules.ejs
Normal file
110
web/src/views/email_rules.ejs
Normal file
@@ -0,0 +1,110 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Email Rules</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1200px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
input,select{padding:12px;border-radius:10px;border:1px solid #233043;background:#0e1520;color:#e8eef7;width:100%}
|
||||
button{padding:12px 14px;border-radius:10px;border:1px solid #233043;background:#1c2a3d;color:#e8eef7}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{border-bottom:1px solid #233043;padding:10px 6px;text-align:left;color:#a9b7c6;font-size:13px;vertical-align:top}
|
||||
th{color:#e8eef7}
|
||||
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}
|
||||
.col4{grid-column:span 12}
|
||||
@media(min-width:900px){.col4{grid-column:span 4}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700">Email Rules</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/admin/email">Email Accounts</a>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Create rule</h2>
|
||||
<form method="post" action="/admin/email-rules/create" class="grid">
|
||||
<div class="col4">
|
||||
<label class="muted">Project</label>
|
||||
<select name="projectId" required>
|
||||
<% projects.forEach(p => { %>
|
||||
<option value="<%= p.id %>"><%= p.name %> (<%= p.job_number || '—' %>)</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col4">
|
||||
<label class="muted">Match type</label>
|
||||
<select name="matchType">
|
||||
<option value="from_domain">From domain</option>
|
||||
<option value="from_contains">From contains</option>
|
||||
<option value="subject_contains">Subject contains</option>
|
||||
<option value="body_contains">Body contains</option>
|
||||
<option value="thread_key">Thread key</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col4">
|
||||
<label class="muted">Match value</label>
|
||||
<input name="matchValue" placeholder="@gc.com or 0222600001 or project name" required />
|
||||
</div>
|
||||
<div class="col4">
|
||||
<label class="muted">Priority (lower = earlier)</label>
|
||||
<input name="priority" value="100" />
|
||||
</div>
|
||||
<div style="grid-column:span 12">
|
||||
<button type="submit">Add rule</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Rules</h2>
|
||||
<div style="overflow:auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Enabled</th>
|
||||
<th>Priority</th>
|
||||
<th>Project</th>
|
||||
<th>Type</th>
|
||||
<th>Value</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% rules.forEach(r => { %>
|
||||
<tr>
|
||||
<td><%= r.enabled ? 'yes' : 'no' %></td>
|
||||
<td><%= r.priority %></td>
|
||||
<td><%= r.project_name %></td>
|
||||
<td><%= r.match_type %></td>
|
||||
<td><%= r.match_value %></td>
|
||||
<td>
|
||||
<form method="post" action="/admin/email-rules/<%= r.id %>/toggle" style="display:inline">
|
||||
<button type="submit"><%= r.enabled ? 'Disable' : 'Enable' %></button>
|
||||
</form>
|
||||
<form method="post" action="/admin/email-rules/<%= r.id %>/delete" onsubmit="return confirm('Delete rule?');" style="display:inline">
|
||||
<button type="submit" style="background:#2b1216;border-color:#5a1e28">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
86
web/src/views/email_settings.ejs
Normal file
86
web/src/views/email_settings.ejs
Normal file
@@ -0,0 +1,86 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Email Accounts</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1100px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{border-bottom:1px solid #233043;padding:10px 6px;text-align:left;color:#a9b7c6;font-size:13px;vertical-align:top}
|
||||
th{color:#e8eef7}
|
||||
button{padding:10px 12px;border-radius:10px;border:1px solid #233043;background:#1c2a3d;color:#e8eef7}
|
||||
.pill{display:inline-block;padding:2px 8px;border:1px solid #233043;border-radius:999px;font-size:12px;color:#a9b7c6}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700">Email Accounts</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/admin/email-rules">Rules</a>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="card">
|
||||
<p class="muted">Connectors are first-class from day 1. They’ll show <b>Not configured</b> until you provide OAuth credentials. Manual .eml upload works now.</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Connectors</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Provider</th>
|
||||
<th>Enabled</th>
|
||||
<th>Configured</th>
|
||||
<th>Authorized</th>
|
||||
<th>Last sync</th>
|
||||
<th>Last error</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% connectors.forEach(c => { %>
|
||||
<tr>
|
||||
<td><b><%= c.provider %></b></td>
|
||||
<td><span class="pill"><%= c.enabled ? 'yes' : 'no' %></span></td>
|
||||
<td><span class="pill"><%= c.configured ? 'yes' : 'no' %></span></td>
|
||||
<td><span class="pill"><%= c.authorized ? 'yes' : 'no' %></span></td>
|
||||
<td><%= c.last_sync_at || '' %></td>
|
||||
<td><%= c.last_error || '' %></td>
|
||||
<td>
|
||||
<form method="post" action="/admin/email-connectors/<%= c.provider %>/toggle">
|
||||
<button type="submit"><%= c.enabled ? 'Disable' : 'Enable' %></button>
|
||||
</form>
|
||||
<div class="muted" style="margin-top:6px">
|
||||
<% if (!c.configured) { %>
|
||||
Not configured — add OAuth credentials in <code>.env</code> (we’ll add UI later).
|
||||
<% } else if (!c.authorized) { %>
|
||||
Configured — needs OAuth authorization (pending).
|
||||
<% } else { %>
|
||||
Connected.
|
||||
<% } %>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Manual import</h2>
|
||||
<p><a href="/inbox">→ Inbox import + triage</a></p>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
73
web/src/views/home.ejs
Normal file
73
web/src/views/home.ejs
Normal file
@@ -0,0 +1,73 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Dashboard</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{position:sticky;top:0;background:#111824;border-bottom:1px solid #233043;padding:14px 16px}
|
||||
main{max-width:1100px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
.muted{color:#a9b7c6}
|
||||
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}
|
||||
.col6{grid-column:span 12}
|
||||
@media(min-width:900px){.col6{grid-column:span 6}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div>
|
||||
<div style="font-weight:700">Mastermind</div>
|
||||
<div class="muted" style="font-size:13px">Logged in as <%= user.email || user.display_name || user.id %></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/account/password">Password</a>
|
||||
<a href="/setup">Setup</a>
|
||||
<a href="/docs">Docs</a>
|
||||
<% if (user.role === 'owner') { %>
|
||||
<a href="/admin/email">Email</a>
|
||||
<a href="/admin/email-rules">Rules</a>
|
||||
<a href="/admin/users">Users</a>
|
||||
<a href="/admin/audit">Audit</a>
|
||||
<% } %>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="grid">
|
||||
<div class="card col6">
|
||||
<h2 style="margin:0 0 8px">MVP status</h2>
|
||||
<div class="muted">Auth is wired (local + optional Google/Microsoft). Next: project wizard + inbox ingestion.</div>
|
||||
</div>
|
||||
<div class="card col6">
|
||||
<h2 style="margin:0 0 8px">Base URL</h2>
|
||||
<div class="muted">Current: <code><%= baseUrl %></code></div>
|
||||
<div class="muted" style="margin-top:6px">If OAuth callbacks don’t work, update Base URL under Setup.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Start</h2>
|
||||
<p class="muted">Create a project profile so Mastermind can sort your single mailbox automatically.</p>
|
||||
<p><a href="/projects">→ Projects</a></p>
|
||||
<p><a href="/inbox">→ Inbox (import + triage)</a></p>
|
||||
<p><a href="/drafts">→ Drafts (PCO / RFI)</a></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Next build steps</h2>
|
||||
<ol class="muted">
|
||||
<li>Inbox triage queue (single mailbox → auto-sort per project)</li>
|
||||
<li>RFI/Submittal/PCO logs + draft generators with citations</li>
|
||||
<li>Email connectors (Gmail + Microsoft) behind OAuth + local fallback imports</li>
|
||||
</ol>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
114
web/src/views/inbox.ejs
Normal file
114
web/src/views/inbox.ejs
Normal file
@@ -0,0 +1,114 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Inbox</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1200px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
input,select{padding:12px;border-radius:10px;border:1px solid #233043;background:#0e1520;color:#e8eef7;width:100%}
|
||||
button{padding:12px 14px;border-radius:10px;border:1px solid #233043;background:#1c2a3d;color:#e8eef7}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{border-bottom:1px solid #233043;padding:10px 6px;text-align:left;color:#a9b7c6;font-size:13px;vertical-align:top}
|
||||
th{color:#e8eef7}
|
||||
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}
|
||||
.col6{grid-column:span 12}
|
||||
@media(min-width:900px){.col6{grid-column:span 6}}
|
||||
.actions{display:flex;gap:8px;flex-wrap:wrap}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700">Inbox</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/projects">Projects</a>
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Import emails (until OAuth is connected)</h2>
|
||||
<div class="muted">Upload .eml files. Mastermind stores them and puts them into the Unsorted queue for triage.</div>
|
||||
<form method="post" action="/inbox/upload" enctype="multipart/form-data" class="grid" style="margin-top:10px">
|
||||
<div class="col6">
|
||||
<label class="muted">.eml files</label>
|
||||
<input type="file" name="emls" multiple accept=".eml,message/rfc822" required />
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">Source label</label>
|
||||
<input name="source" placeholder="outlook-export" value="upload" />
|
||||
</div>
|
||||
<div style="grid-column:span 12">
|
||||
<button type="submit">Import</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Unsorted</h2>
|
||||
<div class="muted">Assign each email to a project. Later this will be automated with rules + OAuth sync.</div>
|
||||
<div style="overflow:auto;margin-top:10px">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>From</th>
|
||||
<th>Subject</th>
|
||||
<th>Assign</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% emails.forEach(e => { %>
|
||||
<tr>
|
||||
<td><%= e.date || '' %></td>
|
||||
<td><%= e.from_addr || '' %></td>
|
||||
<td><a href="/inbox/<%= e.id %>"><%= e.subject || '(no subject)' %></a></td>
|
||||
<td>
|
||||
<form method="post" action="/inbox/<%= e.id %>/assign" class="actions" style="margin-bottom:8px">
|
||||
<select name="projectId" required>
|
||||
<option value="" disabled selected>Select project…</option>
|
||||
<% projects.forEach(p => { %>
|
||||
<option value="<%= p.id %>"><%= p.name %> (<%= p.job_number || '—' %>)</option>
|
||||
<% }) %>
|
||||
</select>
|
||||
<button type="submit">Assign</button>
|
||||
</form>
|
||||
|
||||
<div class="muted" style="font-size:12px;margin-bottom:6px">Make a rule from this email:</div>
|
||||
<form method="post" action="/inbox/<%= e.id %>/rule/from_domain" class="actions" style="margin-bottom:8px">
|
||||
<select name="projectId" required>
|
||||
<option value="" disabled selected>Project…</option>
|
||||
<% projects.forEach(p => { %>
|
||||
<option value="<%= p.id %>"><%= p.name %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
<button type="submit">Rule: From domain</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/inbox/<%= e.id %>/rule/subject_job" class="actions">
|
||||
<select name="projectId" required>
|
||||
<option value="" disabled selected>Project…</option>
|
||||
<% projects.forEach(p => { %>
|
||||
<option value="<%= p.id %>"><%= p.name %></option>
|
||||
<% }) %>
|
||||
</select>
|
||||
<button type="submit">Rule: Subject job#</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
88
web/src/views/inbox_email.ejs
Normal file
88
web/src/views/inbox_email.ejs
Normal file
@@ -0,0 +1,88 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Email</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1100px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
pre{white-space:pre-wrap;background:#0e1520;border:1px solid #233043;border-radius:10px;padding:12px;color:#d6e3f3}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{border-bottom:1px solid #233043;padding:10px 6px;text-align:left;color:#a9b7c6;font-size:13px;vertical-align:top}
|
||||
th{color:#e8eef7}
|
||||
button{padding:10px 12px;border-radius:10px;border:1px solid #233043;background:#1c2a3d;color:#e8eef7}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700">Email</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/inbox">Inbox</a>
|
||||
<a href="/projects">Projects</a>
|
||||
<a href="/">Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="card">
|
||||
<div class="muted"><b>Date:</b> <%= email.date || '' %></div>
|
||||
<div class="muted"><b>From:</b> <%= email.from_addr || '' %></div>
|
||||
<div class="muted"><b>To:</b> <%= email.to_addr || '' %></div>
|
||||
<div class="muted"><b>Subject:</b> <%= email.subject || '' %></div>
|
||||
<div class="muted"><b>Status:</b> <%= email.status %></div>
|
||||
<div class="muted"><b>Raw file:</b> <%= email.raw_path || '' %></div>
|
||||
|
||||
<div style="height:10px"></div>
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap">
|
||||
<form method="post" action="/drafts/pco/from-email">
|
||||
<input type="hidden" name="emailId" value="<%= email.id %>" />
|
||||
<button type="submit">Create PCO draft from this email</button>
|
||||
</form>
|
||||
<form method="post" action="/drafts/rfi/from-email">
|
||||
<input type="hidden" name="emailId" value="<%= email.id %>" />
|
||||
<button type="submit">Create RFI draft from this email</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Attachments</h2>
|
||||
<% if (!attachments.length) { %>
|
||||
<div class="muted">No attachments stored for this email.</div>
|
||||
<% } else { %>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Filename</th>
|
||||
<th>Type</th>
|
||||
<th>Size</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% attachments.forEach(a => { %>
|
||||
<tr>
|
||||
<td><%= a.filename || '' %></td>
|
||||
<td><%= a.content_type || '' %></td>
|
||||
<td><%= a.size_bytes || '' %></td>
|
||||
<td><a href="/attachments/<%= a.id %>">Download</a></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Body (text)</h2>
|
||||
<pre><%= email.body_text || '' %></pre>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
49
web/src/views/login.ejs
Normal file
49
web/src/views/login.ejs
Normal file
@@ -0,0 +1,49 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Login</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
.wrap{max-width:420px;margin:0 auto;padding:28px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:16px}
|
||||
label{display:block;margin:10px 0 6px;color:#a9b7c6}
|
||||
input{width:100%;padding:12px;border-radius:10px;border:1px solid #233043;background:#0e1520;color:#e8eef7}
|
||||
button{width:100%;padding:12px;border-radius:10px;border:1px solid #233043;background:#1c2a3d;color:#e8eef7;margin-top:12px}
|
||||
.row{display:flex;gap:10px;margin-top:12px}
|
||||
.row a{flex:1;text-align:center;text-decoration:none;padding:12px;border-radius:10px;border:1px solid #233043;color:#7cc4ff;background:#0e1520}
|
||||
.muted{color:#a9b7c6;font-size:13px;line-height:1.4}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1 style="margin:0 0 8px">Mastermind</h1>
|
||||
<div class="muted">Local login + Google/Microsoft OAuth. Mobile friendly.</div>
|
||||
<div style="height:12px"></div>
|
||||
|
||||
<div class="card">
|
||||
<form method="post" action="/login">
|
||||
<label>Email</label>
|
||||
<input name="email" placeholder="you@company.com" />
|
||||
<label>Password</label>
|
||||
<input name="password" type="password" placeholder="••••••••" />
|
||||
<button type="submit">Sign in (local)</button>
|
||||
</form>
|
||||
|
||||
<div class="row">
|
||||
<% if (googleEnabled) { %>
|
||||
<a href="/auth/google">Google</a>
|
||||
<% } %>
|
||||
<% if (microsoftEnabled) { %>
|
||||
<a href="/auth/microsoft">Microsoft</a>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
<p class="muted" style="margin-top:12px">
|
||||
First run creates <b>owner@local / owner</b> (change ASAP).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
80
web/src/views/project_detail.ejs
Normal file
80
web/src/views/project_detail.ejs
Normal file
@@ -0,0 +1,80 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Project</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1100px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}
|
||||
.col6{grid-column:span 12}
|
||||
@media(min-width:900px){.col6{grid-column:span 6}}
|
||||
input,select{padding:12px;border-radius:10px;border:1px solid #233043;background:#0e1520;color:#e8eef7;width:100%}
|
||||
button{padding:12px 14px;border-radius:10px;border:1px solid #233043;background:#1c2a3d;color:#e8eef7}
|
||||
.pill{display:inline-block;padding:2px 8px;border:1px solid #233043;border-radius:999px;font-size:12px;color:#a9b7c6}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div>
|
||||
<div style="font-weight:700"><%= project.name %></div>
|
||||
<div class="muted" style="font-size:13px">Job: <%= project.job_number || '—' %> · Mode: <span class="pill"><%= project.role_mode %></span></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/projects/<%= project.id %>/inbox">Inbox</a>
|
||||
<a href="/projects">Projects</a>
|
||||
<a href="/">Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Project profile</h2>
|
||||
<form method="post" action="/projects/<%= project.id %>/update" class="grid">
|
||||
<div class="col6">
|
||||
<label class="muted">Name</label>
|
||||
<input name="name" value="<%= project.name %>" required />
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">Job #</label>
|
||||
<input name="jobNumber" value="<%= project.job_number || '' %>" />
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">Mode</label>
|
||||
<select name="roleMode">
|
||||
<option value="ec" <%= project.role_mode==='ec'?'selected':'' %>>Electrical contractor</option>
|
||||
<option value="gc" <%= project.role_mode==='gc'?'selected':'' %>>General contractor</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">GC name</label>
|
||||
<input name="gcName" value="<%= project.gc_name || '' %>" />
|
||||
</div>
|
||||
<div style="grid-column:span 12">
|
||||
<label class="muted">Keywords (used for email sorting)</label>
|
||||
<input name="keywords" value="<%= project.keywords || '' %>" />
|
||||
</div>
|
||||
<div style="grid-column:span 12">
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Next modules</h2>
|
||||
<ul class="muted">
|
||||
<li>Inbox for this project (auto-sorted from the single mailbox)</li>
|
||||
<li>Logs: RFI / Submittals / PCO / Procurement</li>
|
||||
<li>Draft tools: create RFI or PCO from an email</li>
|
||||
</ul>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
63
web/src/views/project_inbox.ejs
Normal file
63
web/src/views/project_inbox.ejs
Normal file
@@ -0,0 +1,63 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Project Inbox</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1200px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{border-bottom:1px solid #233043;padding:10px 6px;text-align:left;color:#a9b7c6;font-size:13px;vertical-align:top}
|
||||
th{color:#e8eef7}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div>
|
||||
<div style="font-weight:700"><%= project.name %> — Inbox</div>
|
||||
<div class="muted" style="font-size:13px">Job: <%= project.job_number || '—' %></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/projects/<%= project.id %>">Project</a>
|
||||
<a href="/projects">Projects</a>
|
||||
<a href="/inbox">Global Inbox</a>
|
||||
<a href="/">Dashboard</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Assigned emails</h2>
|
||||
<div style="overflow:auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>From</th>
|
||||
<th>Subject</th>
|
||||
<th>Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% emails.forEach(e => { %>
|
||||
<tr>
|
||||
<td><%= e.date || '' %></td>
|
||||
<td><%= e.from_addr || '' %></td>
|
||||
<td><a href="/inbox/<%= e.id %>"><%= e.subject || '(no subject)' %></a></td>
|
||||
<td><%= e.source %></td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
113
web/src/views/projects.ejs
Normal file
113
web/src/views/projects.ejs
Normal file
@@ -0,0 +1,113 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Projects</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px;position:sticky;top:0}
|
||||
main{max-width:1100px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
.muted{color:#a9b7c6}
|
||||
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:12px}
|
||||
.col6{grid-column:span 12}
|
||||
@media(min-width:900px){.col6{grid-column:span 6}}
|
||||
input,select{padding:12px;border-radius:10px;border:1px solid #233043;background:#0e1520;color:#e8eef7;width:100%}
|
||||
button{padding:12px 14px;border-radius:10px;border:1px solid #233043;background:#1c2a3d;color:#e8eef7}
|
||||
table{width:100%;border-collapse:collapse}
|
||||
th,td{border-bottom:1px solid #233043;padding:10px 6px;text-align:left;color:#a9b7c6;font-size:13px;vertical-align:top}
|
||||
th{color:#e8eef7}
|
||||
.pill{display:inline-block;padding:2px 8px;border:1px solid #233043;border-radius:999px;font-size:12px;color:#a9b7c6}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700">Projects</div>
|
||||
<div style="display:flex;gap:10px">
|
||||
<a href="/">Dashboard</a>
|
||||
<a href="/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">New project (2-minute setup)</h2>
|
||||
<div class="muted">Minimum fields: name + job #. Add keywords if sorting needs help.</div>
|
||||
<form method="post" action="/projects/create" class="grid" style="margin-top:10px">
|
||||
<div class="col6">
|
||||
<label class="muted">Project name</label>
|
||||
<input name="name" placeholder="1100 Columbia FA" required />
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">Job #</label>
|
||||
<input name="jobNumber" placeholder="0222600001" />
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">Mode</label>
|
||||
<select name="roleMode">
|
||||
<option value="ec">Electrical contractor</option>
|
||||
<option value="gc">General contractor</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">GC name</label>
|
||||
<input name="gcName" placeholder="GC / Construction Manager" />
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">City</label>
|
||||
<input name="city" placeholder="Chapel Hill" />
|
||||
</div>
|
||||
<div class="col6">
|
||||
<label class="muted">State</label>
|
||||
<input name="state" placeholder="NC" />
|
||||
</div>
|
||||
<div style="grid-column:span 12">
|
||||
<label class="muted">Keywords (optional)</label>
|
||||
<input name="keywords" placeholder="owner name, building nickname, address fragments, common subject tags" />
|
||||
</div>
|
||||
<div style="grid-column:span 12">
|
||||
<button type="submit">Create project</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Your projects</h2>
|
||||
<div style="overflow:auto">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Job #</th>
|
||||
<th>Mode</th>
|
||||
<th>GC</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% projects.forEach(p => { %>
|
||||
<tr>
|
||||
<td><a href="/projects/<%= p.id %>"><%= p.name %></a></td>
|
||||
<td><%= p.job_number || '' %></td>
|
||||
<td><span class="pill"><%= p.role_mode %></span></td>
|
||||
<td><%= p.gc_name || '' %></td>
|
||||
<td><%= p.active ? 'active' : 'archived' %></td>
|
||||
<td>
|
||||
<form method="post" action="/projects/<%= p.id %>/toggle" style="display:inline">
|
||||
<button type="submit"><%= p.active ? 'Archive' : 'Unarchive' %></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<% }) %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
37
web/src/views/setup.ejs
Normal file
37
web/src/views/setup.ejs
Normal file
@@ -0,0 +1,37 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Mastermind — Setup</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;margin:0;background:#0b0f14;color:#e8eef7}
|
||||
header{background:#111824;border-bottom:1px solid #233043;padding:14px 16px}
|
||||
main{max-width:700px;margin:0 auto;padding:16px}
|
||||
.card{background:#111824;border:1px solid #233043;border-radius:12px;padding:14px;margin:12px 0}
|
||||
label{display:block;margin:10px 0 6px;color:#a9b7c6}
|
||||
input{width:100%;padding:12px;border-radius:10px;border:1px solid #233043;background:#0e1520;color:#e8eef7}
|
||||
button{padding:12px 14px;border-radius:10px;border:1px solid #233043;background:#1c2a3d;color:#e8eef7;margin-top:12px}
|
||||
a{color:#7cc4ff;text-decoration:none}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:10px">
|
||||
<div style="font-weight:700">Setup</div>
|
||||
<a href="/">Back</a>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<div class="card">
|
||||
<h2 style="margin:0 0 8px">Public Base URL</h2>
|
||||
<div style="color:#a9b7c6;font-size:13px;line-height:1.4">This should be the URL you use to reach the app (e.g., Tailscale IP). OAuth callbacks rely on this.</div>
|
||||
<form method="post" action="/setup/base-url">
|
||||
<label>Base URL</label>
|
||||
<input name="baseUrl" value="<%= baseUrl %>" />
|
||||
<button type="submit">Save</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user