Skip to content
Merged
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
10 changes: 9 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,16 @@ DB_USER=test
DB_PASSWORD=test
DB_NAME=scriptio

# Staging Basic Auth (htpasswd format, escape $ as $$ in this file)
# Basic Auth (htpasswd format, escape $ as $$ in this file)
PROD_AUTH_USERS=
STAGING_AUTH_USERS=
MONITORING_AUTH_USERS=

# Monitoring
GRAFANA_ADMIN_PASSWORD=
METRICS_BEARER_TOKEN=
APP_MEMORY_LIMIT_BYTES=536870912 # 512 MiB; tune to the prod container's memory budget
DB_SIZE_LIMIT_BYTES=10737418240 # 10 GiB; tune to the prod DB's expected ceiling

# S3 storage
S3_BUCKET=
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ EXPOSE 3000
USER node
ENV NEXT_TELEMETRY_DISABLED 1

ENTRYPOINT ["./launch.sh"]
ENTRYPOINT ["./scripts/launch.sh"]
CMD [ "npm", "start" ]
133 changes: 133 additions & 0 deletions components/admin/AdminShell.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
.wrapper {
display: flex;
width: 100%;
height: 100vh;
background: var(--main-bg);
}

.sidebar {
width: 240px;
flex-shrink: 0;
background: var(--secondary);
border-right: 1px solid var(--separator);
display: flex;
flex-direction: column;
padding: 24px 16px;
gap: 24px;
}

.brand {
display: flex;
align-items: center;
gap: 10px;
padding: 0 8px;
color: var(--primary-text);
font-family: var(--font-josefin), sans-serif;
font-weight: 700;
font-size: 18px;
letter-spacing: 0.02em;
}

.brandBadge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
background: var(--tertiary);
color: var(--primary-text);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
font-family: var(--font-inter), sans-serif;
font-weight: 600;
}

.nav {
display: flex;
flex-direction: column;
gap: 2px;
}

.navGroupLabel {
font-family: var(--font-inter), sans-serif;
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--secondary-text);
padding: 12px 12px 4px;
}

.navItem {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: transparent;
border: none;
border-radius: 8px;
color: var(--primary-text);
font-family: var(--font-inter), sans-serif;
font-size: 14px;
cursor: pointer;
text-align: left;
text-decoration: none;
transition: background-color 120ms ease;
}

.navItem:hover {
background: var(--primary-hover);
}

.navItemActive {
background: var(--tertiary);
}

.navItemActive:hover {
background: var(--tertiary-hover);
}

.footer {
margin-top: auto;
padding: 12px;
border-top: 1px solid var(--separator);
color: var(--secondary-text);
font-size: 12px;
font-family: var(--font-inter), sans-serif;
}

.footerEmail {
color: var(--primary-text);
font-weight: 600;
display: block;
margin-bottom: 2px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.main {
flex: 1;
overflow-y: auto;
padding: 32px 40px;
min-width: 0;
}

.header {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 24px;
}

.title {
color: var(--primary-text);
font-family: var(--font-josefin), sans-serif;
font-size: 28px;
font-weight: 700;
}

.subtitle {
color: var(--secondary-text);
font-family: var(--font-inter), sans-serif;
font-size: 13px;
}
72 changes: 72 additions & 0 deletions components/admin/AdminShell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";

import { ReactNode } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { BarChart3, Users, FolderOpen, LogOut } from "lucide-react";
import styles from "./AdminShell.module.css";

type NavLink = { href: string; label: string; icon: ReactNode };

const NAV_LINKS: NavLink[] = [
{ href: "/admin", label: "Overview", icon: <BarChart3 size={16} /> },
{ href: "/admin/users", label: "Users", icon: <Users size={16} /> },
{ href: "/admin/projects", label: "Projects", icon: <FolderOpen size={16} /> },
];

type Props = {
email: string;
title: string;
subtitle?: string;
children: ReactNode;
};

export default function AdminShell({ email, title, subtitle, children }: Props) {
const pathname = usePathname() ?? "";

return (
<div className={styles.wrapper}>
<aside className={styles.sidebar}>
<div className={styles.brand}>
Scriptio
<span className={styles.brandBadge}>Admin</span>
</div>
<nav className={styles.nav}>
<div className={styles.navGroupLabel}>Monitoring</div>
{NAV_LINKS.map((link) => {
const active =
link.href === "/admin"
? pathname === "/admin"
: pathname.startsWith(link.href);
return (
<Link
key={link.href}
href={link.href}
className={`${styles.navItem} ${active ? styles.navItemActive : ""}`}
>
{link.icon}
{link.label}
</Link>
);
})}
</nav>
<div className={styles.footer}>
<span className={styles.footerEmail} title={email}>
{email}
</span>
<Link href="/" className={styles.navItem} style={{ padding: "8px 0" }}>
<LogOut size={14} />
Back to app
</Link>
</div>
</aside>
<main className={styles.main}>
<div className={styles.header}>
<h1 className={styles.title}>{title}</h1>
{subtitle && <span className={styles.subtitle}>{subtitle}</span>}
</div>
{children}
</main>
</div>
);
}
137 changes: 137 additions & 0 deletions components/admin/ProjectDetail.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
.container {
display: flex;
flex-direction: column;
gap: 24px;
}

.backLink {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--secondary-text);
text-decoration: none;
font-family: var(--font-inter), sans-serif;
font-size: 13px;
margin-bottom: 8px;
width: fit-content;
}

.backLink:hover {
color: var(--primary-text);
}

.card {
background: var(--secondary);
border: 1px solid var(--separator);
border-radius: 12px;
padding: 24px;
box-shadow: var(--panel-shadow);
}

.cardTitle {
color: var(--primary-text);
font-family: var(--font-josefin), sans-serif;
font-size: 18px;
font-weight: 700;
margin-bottom: 16px;
}

.fields {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px 24px;
}

.field {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}

.fieldLabel {
color: var(--secondary-text);
font-family: var(--font-inter), sans-serif;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}

.fieldValue {
color: var(--primary-text);
font-family: var(--font-inter), sans-serif;
font-size: 14px;
overflow-wrap: anywhere;
}

.idValue {
font-family: var(--font-courier), monospace;
font-size: 13px;
}

.table {
display: flex;
flex-direction: column;
border: 1px solid var(--separator);
border-radius: 8px;
overflow: hidden;
}

.tableRow {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 16px;
padding: 12px 16px;
font-family: var(--font-inter), sans-serif;
font-size: 13px;
color: var(--primary-text);
border-bottom: 1px solid var(--separator);
align-items: center;
}

.tableRow:last-child {
border-bottom: none;
}

.tableHeader {
background: var(--primary);
color: var(--secondary-text);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
}

.tableRowLink {
text-decoration: none;
transition: background-color 120ms ease;
cursor: pointer;
}

.tableRowLink:hover {
background: var(--primary-hover);
}

.tableCellMuted {
color: var(--secondary-text);
font-size: 12px;
}

.emptyRow {
padding: 16px;
text-align: center;
color: var(--secondary-text);
font-family: var(--font-inter), sans-serif;
font-size: 13px;
}

.message {
color: var(--secondary-text);
font-family: var(--font-inter), sans-serif;
font-size: 14px;
}

.error {
color: var(--error);
}
Loading