feat: add basic website

Signed-off-by: kjuulh <contact@kjuulh.io>
This commit is contained in:
2026-03-07 19:46:13 +01:00
commit b439762877
71 changed files with 16576 additions and 0 deletions

91
templates/base.html.jinja Normal file
View File

@@ -0,0 +1,91 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }}</title>
<meta name="description" content="{{ description }}">
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body class="bg-white text-gray-900 antialiased">
<nav class="border-b border-gray-200">
<div class="max-w-6xl mx-auto px-4 py-4 flex items-center justify-between">
{% if user is defined and user %}
{# Authenticated nav #}
<div class="flex items-center gap-6">
<a href="/dashboard" class="text-xl font-bold tracking-tight">forage</a>
{% if current_org is defined and current_org %}
<span class="text-sm text-gray-400">/</span>
<span class="text-sm font-medium">{{ current_org }}</span>
{% endif %}
</div>
<div class="flex items-center gap-6">
{% if current_org is defined and current_org %}
<a href="/orgs/{{ current_org }}/projects" class="text-sm text-gray-600 hover:text-gray-900">Projects</a>
<a href="/orgs/{{ current_org }}/usage" class="text-sm text-gray-600 hover:text-gray-900">Usage</a>
{% endif %}
{% if orgs is defined and orgs | length > 1 %}
<details class="relative">
<summary class="text-sm text-gray-600 hover:text-gray-900 cursor-pointer list-none">
Switch org
</summary>
<div class="absolute right-0 mt-2 w-48 bg-white border border-gray-200 rounded-md shadow-lg z-10">
{% for org in orgs %}
<a href="/orgs/{{ org.name }}/projects" class="block px-4 py-2 text-sm hover:bg-gray-50">{{ org.name }}</a>
{% endfor %}
</div>
</details>
{% endif %}
<a href="/settings/tokens" class="text-sm text-gray-600 hover:text-gray-900">Tokens</a>
<form method="POST" action="/logout" class="inline">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<button type="submit" class="text-sm text-gray-600 hover:text-gray-900">Sign out</button>
</form>
</div>
{% else %}
{# Marketing nav #}
<a href="/" class="text-xl font-bold tracking-tight">forage</a>
<div class="flex items-center gap-6">
<a href="/pricing" class="text-sm text-gray-600 hover:text-gray-900">Pricing</a>
<a href="/components" class="text-sm text-gray-600 hover:text-gray-900">Components</a>
<a href="/login" class="text-sm font-medium px-4 py-2 bg-gray-900 text-white rounded-md hover:bg-gray-800">Sign in</a>
</div>
{% endif %}
</div>
</nav>
<main>
{% block content %}{% endblock %}
</main>
<footer class="border-t border-gray-200 mt-24">
<div class="max-w-6xl mx-auto px-4 py-12">
<div class="grid grid-cols-2 md:grid-cols-3 gap-8">
<div>
<h3 class="font-bold text-sm mb-3">Product</h3>
<ul class="space-y-2 text-sm text-gray-600">
<li><a href="/pricing" class="hover:text-gray-900">Pricing</a></li>
<li><a href="/components" class="hover:text-gray-900">Components</a></li>
</ul>
</div>
<div>
<h3 class="font-bold text-sm mb-3">Platform</h3>
<ul class="space-y-2 text-sm text-gray-600">
<li><a href="/signup" class="hover:text-gray-900">Get Started</a></li>
<li><a href="/login" class="hover:text-gray-900">Sign In</a></li>
</ul>
</div>
<div>
<h3 class="font-bold text-sm mb-3">Forest</h3>
<ul class="space-y-2 text-sm text-gray-600">
<li><a href="https://src.rawpotion.io/rawpotion/forest" class="hover:text-gray-900">Forest on Git</a></li>
</ul>
</div>
</div>
<div class="mt-12 pt-8 border-t border-gray-200 text-sm text-gray-500">
&copy; 2026 Forage. Built with Forest.
</div>
</div>
</footer>
</body>
</html>

View File

@@ -0,0 +1,19 @@
services:
postgres:
image: postgres:17-alpine
environment:
POSTGRES_DB: forage
POSTGRES_USER: forageuser
POSTGRES_PASSWORD: foragepassword
ports:
- "5432:5432"
volumes:
- forage-pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U forageuser -d forage"]
interval: 5s
timeout: 5s
retries: 5
volumes:
forage-pgdata:

View File

@@ -0,0 +1,57 @@
FROM rust:1.85-bookworm AS builder
RUN apt-get update && \
apt-get install -y clang mold && \
rm -rf /var/lib/apt/lists/*
WORKDIR /mnt/src
# Cargo config for mold linker
RUN mkdir -p /usr/local/cargo && \
printf '[target.x86_64-unknown-linux-gnu]\nlinker = "clang"\nrustflags = ["-C", "link-arg=-fuse-ld=mold"]\n' \
> /usr/local/cargo/config.toml
ENV SQLX_OFFLINE=true
# Copy manifests first for dependency caching
COPY Cargo.toml Cargo.lock ./
COPY crates/forage-server/Cargo.toml crates/forage-server/Cargo.toml
COPY crates/forage-core/Cargo.toml crates/forage-core/Cargo.toml
COPY crates/forage-db/Cargo.toml crates/forage-db/Cargo.toml
# Create skeleton source files for dependency build
RUN mkdir -p crates/forage-server/src && echo 'fn main() {}' > crates/forage-server/src/main.rs && \
mkdir -p crates/forage-core/src && echo '' > crates/forage-core/src/lib.rs && \
mkdir -p crates/forage-db/src && echo '' > crates/forage-db/src/lib.rs
# Build dependencies only (cacheable layer)
RUN cargo build --release -p forage-server 2>/dev/null || true
# Copy real source
COPY crates/ crates/
COPY templates/ templates/
COPY static/ static/
COPY .sqlx/ .sqlx/
# Touch source files to invalidate the skeleton build
RUN find crates -name "*.rs" -exec touch {} +
# Build the real binary
RUN cargo build --release -p forage-server
# Verify it runs
RUN ./target/release/forage-server --help || true
# Runtime image
FROM gcr.io/distroless/cc-debian12:latest
COPY --from=builder /mnt/src/target/release/forage-server /usr/local/bin/forage-server
COPY --from=builder /mnt/src/templates /templates
COPY --from=builder /mnt/src/static /static
WORKDIR /
ENV FORAGE_TEMPLATES_PATH=/templates
EXPOSE 3000
ENTRYPOINT ["/usr/local/bin/forage-server"]

View File

@@ -0,0 +1,28 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-4xl mx-auto px-4 pt-24 pb-8 text-center">
<h1 class="text-4xl font-bold tracking-tight">Component Registry</h1>
<p class="mt-4 text-lg text-gray-600">
Discover and share reusable forest components. Deployment patterns,
service templates, and infrastructure modules - all in one place.
</p>
<p class="mt-2 text-sm text-gray-400">The component registry is coming soon.</p>
</section>
<section class="max-w-4xl mx-auto px-4 py-12">
<div class="p-8 border border-gray-200 rounded-lg text-center">
<p class="text-gray-500 mb-4">
Components will be browsable here once the registry is live.
In the meantime, you can publish components via the forest CLI.
</p>
<div class="bg-gray-950 rounded-md p-4 text-sm font-mono text-gray-300 text-left max-w-lg mx-auto">
<pre><span class="text-gray-500"># Publish a component</span>
forest components publish ./my-component
<span class="text-gray-500"># Use a component</span>
forest components add forage/service@1.0</pre>
</div>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,57 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-4xl mx-auto px-4 pt-12">
<div class="flex items-center justify-between mb-8">
<div>
<h1 class="text-2xl font-bold">Welcome, {{ user.username }}</h1>
<p class="text-sm text-gray-600 mt-1">{{ user.emails[0] if user.emails }}</p>
</div>
<div class="flex gap-3">
<a href="/settings/tokens" class="px-4 py-2 text-sm border border-gray-300 rounded-md hover:border-gray-400">
API Tokens
</a>
<form method="POST" action="/logout">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<button type="submit" class="px-4 py-2 text-sm border border-gray-300 rounded-md hover:border-gray-400">
Sign out
</button>
</form>
</div>
</div>
<div class="p-6 border border-gray-200 rounded-lg mb-6">
<p class="text-sm text-gray-500">
Forage is in early access. Container deployments and the component registry
are under active development. You can manage your API tokens now and deploy
once the platform is live.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="p-6 border border-gray-200 rounded-lg">
<h2 class="font-bold text-lg mb-4">Projects</h2>
<p class="text-sm text-gray-600">No projects yet. Deploy your first forest.cue manifest to get started.</p>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<h2 class="font-bold text-lg mb-4">Organisations</h2>
<p class="text-sm text-gray-600">You're not part of any organisation yet.</p>
</div>
</div>
<div class="mt-8 p-6 border border-gray-200 rounded-lg">
<h2 class="font-bold text-lg mb-4">Quick start</h2>
<div class="bg-gray-950 rounded-md p-4 text-sm font-mono text-gray-300">
<pre><span class="text-gray-500"># Install forest CLI</span>
cargo install forest
<span class="text-gray-500"># Create a project</span>
forest init my-project --component forage/service
<span class="text-gray-500"># Deploy</span>
forest release create --env dev</pre>
</div>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,10 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-lg mx-auto px-4 pt-24 text-center">
<p class="text-6xl font-bold text-gray-300">{{ status }}</p>
<h1 class="mt-4 text-2xl font-bold">{{ heading }}</h1>
<p class="mt-2 text-gray-600">{{ message }}</p>
<a href="/" class="inline-block mt-8 text-sm text-gray-500 hover:text-gray-700">&larr; Back to home</a>
</section>
{% endblock %}

View File

@@ -0,0 +1,104 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-4xl mx-auto px-4 pt-24 pb-16 text-center">
<h1 class="text-5xl font-bold tracking-tight leading-tight">
Push a manifest.<br>Get production infrastructure.
</h1>
<p class="mt-6 text-xl text-gray-600 max-w-2xl mx-auto">
Forage is the managed platform for Forest. Define your infrastructure in a
<code class="bg-gray-100 px-2 py-0.5 rounded text-base">forest.cue</code> file,
push it, and we handle the rest.
</p>
<div class="mt-10 flex items-center justify-center gap-4">
<a href="/signup" class="px-6 py-3 bg-gray-900 text-white rounded-md font-medium hover:bg-gray-800">
Get started free
</a>
<a href="/pricing" class="px-6 py-3 border border-gray-300 rounded-md font-medium hover:border-gray-400">
View pricing
</a>
</div>
</section>
<section class="max-w-4xl mx-auto px-4 py-16">
<div class="bg-gray-950 rounded-lg p-6 text-sm font-mono text-gray-300 overflow-x-auto">
<pre><span class="text-gray-500">// forest.cue</span>
<span class="text-blue-400">project</span>: {
<span class="text-green-400">name</span>: <span class="text-amber-400">"my-api"</span>
<span class="text-green-400">organisation</span>: <span class="text-amber-400">"acme"</span>
}
<span class="text-blue-400">dependencies</span>: {
<span class="text-amber-400">"forage/service"</span>: <span class="text-green-400">version</span>: <span class="text-amber-400">"1.0"</span>
<span class="text-amber-400">"forage/postgres"</span>: <span class="text-green-400">version</span>: <span class="text-amber-400">"1.0"</span>
}
<span class="text-blue-400">forage</span>: <span class="text-blue-400">service</span>: {
<span class="text-green-400">config</span>: {
<span class="text-green-400">name</span>: <span class="text-amber-400">"my-api"</span>
<span class="text-green-400">image</span>: <span class="text-amber-400">"my-api:latest"</span>
<span class="text-green-400">ports</span>: [{ <span class="text-green-400">container</span>: <span class="text-purple-400">8080</span>, <span class="text-green-400">protocol</span>: <span class="text-amber-400">"http"</span> }]
}
}</pre>
</div>
<div class="mt-4 text-center">
<code class="text-sm text-gray-500">$ forest release create --env prod</code>
</div>
</section>
<section class="max-w-6xl mx-auto px-4 py-16">
<h2 class="text-3xl font-bold text-center mb-12">Everything you need to ship</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="p-6 border border-gray-200 rounded-lg">
<h3 class="font-bold text-lg mb-2">Component Registry</h3>
<p class="text-gray-600 text-sm">
Publish and discover reusable forest components. Share deployment patterns,
service templates, and infrastructure modules across your organisation.
</p>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<h3 class="font-bold text-lg mb-2">Managed Deployments</h3>
<p class="text-gray-600 text-sm">
Zero-config container runtime. Push your manifest and get automatic scaling,
health checks, rollbacks, and multi-environment support.
</p>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<h3 class="font-bold text-lg mb-2">Container Deployments</h3>
<p class="text-gray-600 text-sm">
Push your containers and let Forage handle the runtime. No Kubernetes
knowledge needed. Pay only for what you use.
</p>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<h3 class="font-bold text-lg mb-2">Type-Safe Infrastructure</h3>
<p class="text-gray-600 text-sm">
CUE gives you typed, validated infrastructure definitions. Catch configuration
errors before they reach production.
</p>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<h3 class="font-bold text-lg mb-2">Team Management</h3>
<p class="text-gray-600 text-sm">
Organisations, role-based access, and audit logs. Manage who can deploy
what, where, and when.
</p>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<h3 class="font-bold text-lg mb-2">Pay As You Go</h3>
<p class="text-gray-600 text-sm">
Transparent, usage-based pricing. Start free, scale smoothly. No surprise
bills, no hidden fees.
</p>
</div>
</div>
</section>
<section class="max-w-4xl mx-auto px-4 py-16 text-center">
<h2 class="text-3xl font-bold mb-4">Ready to simplify your infrastructure?</h2>
<p class="text-gray-600 mb-8">Join the waitlist and be first to try Forage.</p>
<a href="/signup" class="px-6 py-3 bg-gray-900 text-white rounded-md font-medium hover:bg-gray-800">
Get started free
</a>
</section>
{% endblock %}

View File

@@ -0,0 +1,48 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-md mx-auto px-4 pt-24">
<h1 class="text-2xl font-bold text-center mb-8">Sign in to Forage</h1>
{% if error %}
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-md text-sm text-red-700">
{{ error }}
</div>
{% endif %}
<form method="POST" action="/login" class="space-y-4">
<div>
<label for="identifier" class="block text-sm font-medium mb-1">Username or email</label>
<input
type="text"
id="identifier"
name="identifier"
value="{{ identifier }}"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-gray-900"
placeholder="alice or alice@example.com">
</div>
<div>
<label for="password" class="block text-sm font-medium mb-1">Password</label>
<input
type="password"
id="password"
name="password"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-gray-900"
placeholder="Your password">
</div>
<button
type="submit"
class="w-full py-2 bg-gray-900 text-white rounded-md font-medium hover:bg-gray-800">
Sign in
</button>
</form>
<p class="mt-6 text-center text-sm text-gray-600">
Don't have an account? <a href="/signup" class="font-medium text-gray-900 hover:underline">Create one</a>
</p>
</section>
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-4xl mx-auto px-4 pt-12 text-center">
<h1 class="text-2xl font-bold">Welcome to Forage</h1>
<p class="mt-4 text-gray-600">Create your first organisation with the forest CLI to get started.</p>
<div class="mt-8 max-w-lg mx-auto bg-gray-950 rounded-md p-4 text-sm font-mono text-gray-300 text-left">
<pre><span class="text-gray-500"># Install forest CLI</span>
cargo install forest
<span class="text-gray-500"># Create an organisation</span>
forest orgs create my-org
<span class="text-gray-500"># Create a project</span>
forest init my-project --component forage/service
<span class="text-gray-500"># Deploy</span>
forest release create --env dev</pre>
</div>
<div class="mt-8">
<a href="/settings/tokens" class="text-sm text-gray-500 hover:text-gray-700">Manage API tokens &rarr;</a>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,123 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-4xl mx-auto px-4 pt-24 pb-8 text-center">
<h1 class="text-4xl font-bold tracking-tight">Simple, transparent pricing</h1>
<p class="mt-4 text-lg text-gray-600">Start free. Scale when you need to. No surprises.</p>
<p class="mt-2 text-sm text-gray-400">Forage is in early access. Pricing may change.</p>
</section>
<section class="max-w-6xl mx-auto px-4 py-12">
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
<div class="p-6 border border-gray-200 rounded-lg">
<h3 class="font-bold text-lg">Free</h3>
<p class="mt-2 text-3xl font-bold">$0</p>
<p class="text-sm text-gray-500">forever</p>
<ul class="mt-6 space-y-3 text-sm">
<li>1 project</li>
<li>1 environment</li>
<li>256MB RAM, shared CPU</li>
<li>Community components</li>
</ul>
<a href="/signup" class="mt-8 block text-center py-2 border border-gray-300 rounded-md text-sm font-medium hover:border-gray-400">
Get started
</a>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<h3 class="font-bold text-lg">Developer</h3>
<p class="mt-2 text-3xl font-bold">$10<span class="text-base font-normal text-gray-500">/mo</span></p>
<p class="text-sm text-gray-500">per user</p>
<ul class="mt-6 space-y-3 text-sm">
<li>3 projects</li>
<li>3 environments each</li>
<li>512MB RAM, dedicated CPU</li>
<li>Custom domains</li>
</ul>
<a href="/signup" class="mt-8 block text-center py-2 bg-gray-900 text-white rounded-md text-sm font-medium hover:bg-gray-800">
Start trial
</a>
</div>
<div class="p-6 border-2 border-gray-900 rounded-lg relative">
<span class="absolute -top-3 left-4 bg-gray-900 text-white text-xs px-2 py-0.5 rounded">Popular</span>
<h3 class="font-bold text-lg">Team</h3>
<p class="mt-2 text-3xl font-bold">$25<span class="text-base font-normal text-gray-500">/user/mo</span></p>
<p class="text-sm text-gray-500">billed monthly</p>
<ul class="mt-6 space-y-3 text-sm">
<li>Unlimited projects</li>
<li>Unlimited environments</li>
<li>Up to 4GB RAM, 2 vCPU</li>
<li>Private component registry</li>
<li>Team management, RBAC</li>
</ul>
<a href="/signup" class="mt-8 block text-center py-2 bg-gray-900 text-white rounded-md text-sm font-medium hover:bg-gray-800">
Start trial
</a>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<h3 class="font-bold text-lg">Enterprise</h3>
<p class="mt-2 text-3xl font-bold">Custom</p>
<p class="text-sm text-gray-500">tailored to your needs</p>
<ul class="mt-6 space-y-3 text-sm">
<li>Dedicated infrastructure</li>
<li>SLA guarantees</li>
<li>SSO / SAML</li>
<li>Audit logs</li>
<li>Priority support</li>
</ul>
<a href="mailto:sales@forage.sh" class="mt-8 block text-center py-2 border border-gray-300 rounded-md text-sm font-medium hover:border-gray-400">
Contact sales
</a>
</div>
</div>
</section>
<section class="max-w-4xl mx-auto px-4 py-12">
<h2 class="text-2xl font-bold text-center mb-8">Usage-based add-ons</h2>
<div class="overflow-hidden border border-gray-200 rounded-lg">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-6 py-3 font-medium">Resource</th>
<th class="text-left px-6 py-3 font-medium">Price</th>
<th class="text-left px-6 py-3 font-medium">Included free</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<tr>
<td class="px-6 py-3">Compute (vCPU)</td>
<td class="px-6 py-3">$0.05/hour</td>
<td class="px-6 py-3">Varies by plan</td>
</tr>
<tr>
<td class="px-6 py-3">Memory</td>
<td class="px-6 py-3">$0.01/GB-hour</td>
<td class="px-6 py-3">Varies by plan</td>
</tr>
<tr>
<td class="px-6 py-3">Storage</td>
<td class="px-6 py-3">$0.10/GB-month</td>
<td class="px-6 py-3">1GB</td>
</tr>
<tr>
<td class="px-6 py-3">Bandwidth</td>
<td class="px-6 py-3">$0.05/GB</td>
<td class="px-6 py-3">10GB</td>
</tr>
<tr class="text-gray-400">
<td class="px-6 py-3">Managed Databases <span class="text-xs">(coming soon)</span></td>
<td class="px-6 py-3">TBD</td>
<td class="px-6 py-3">-</td>
</tr>
<tr class="text-gray-400">
<td class="px-6 py-3">Managed Services <span class="text-xs">(coming soon)</span></td>
<td class="px-6 py-3">TBD</td>
<td class="px-6 py-3">-</td>
</tr>
</tbody>
</table>
</div>
</section>
{% endblock %}

View File

@@ -0,0 +1,40 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-4xl mx-auto px-4 pt-12">
<div class="flex items-center justify-between mb-8">
<div>
<a href="/orgs/{{ org_name }}/projects" class="text-sm text-gray-500 hover:text-gray-700">&larr; {{ org_name }}</a>
<h1 class="text-2xl font-bold mt-1">{{ project_name }}</h1>
</div>
</div>
<h2 class="font-bold text-lg mb-4">Recent releases</h2>
{% if artifacts %}
<div class="space-y-4">
{% for artifact in artifacts %}
<div class="p-4 border border-gray-200 rounded-lg">
<div class="flex items-center justify-between">
<div>
<p class="font-medium">{{ artifact.title }}</p>
{% if artifact.description %}
<p class="text-sm text-gray-600 mt-1">{{ artifact.description }}</p>
{% endif %}
</div>
<div class="text-right text-sm text-gray-500">
<p class="font-mono">{{ artifact.slug }}</p>
<p>{{ artifact.created_at }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="p-6 border border-gray-200 rounded-lg text-center">
<p class="text-gray-600">No releases yet.</p>
<p class="text-sm text-gray-400 mt-2">Create a release with <code class="bg-gray-100 px-1 rounded">forest release create</code></p>
</div>
{% endif %}
</section>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-4xl mx-auto px-4 pt-12">
<div class="mb-8">
<h1 class="text-2xl font-bold">Projects</h1>
</div>
{% if projects %}
<div class="space-y-4">
{% for project in projects %}
<a href="/orgs/{{ org_name }}/projects/{{ project }}" class="block p-6 border border-gray-200 rounded-lg hover:border-gray-400">
<h2 class="font-bold text-lg">{{ project }}</h2>
</a>
{% endfor %}
</div>
{% else %}
<div class="p-6 border border-gray-200 rounded-lg text-center">
<p class="text-gray-600">No projects yet.</p>
<p class="text-sm text-gray-400 mt-2">Deploy with <code class="bg-gray-100 px-1 rounded">forest release create</code></p>
</div>
{% endif %}
</section>
{% endblock %}

View File

@@ -0,0 +1,75 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-md mx-auto px-4 pt-24">
<h1 class="text-2xl font-bold text-center mb-8">Create your account</h1>
{% if error %}
<div class="mb-6 p-4 bg-red-50 border border-red-200 rounded-md text-sm text-red-700">
{{ error }}
</div>
{% endif %}
<form method="POST" action="/signup" class="space-y-4">
<div>
<label for="username" class="block text-sm font-medium mb-1">Username</label>
<input
type="text"
id="username"
name="username"
value="{{ username }}"
required
minlength="3"
maxlength="64"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-gray-900"
placeholder="alice">
</div>
<div>
<label for="email" class="block text-sm font-medium mb-1">Email</label>
<input
type="email"
id="email"
name="email"
value="{{ email }}"
required
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-gray-900"
placeholder="alice@example.com">
</div>
<div>
<label for="password" class="block text-sm font-medium mb-1">Password</label>
<input
type="password"
id="password"
name="password"
required
minlength="12"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-gray-900"
placeholder="At least 12 characters">
</div>
<div>
<label for="password_confirm" class="block text-sm font-medium mb-1">Confirm password</label>
<input
type="password"
id="password_confirm"
name="password_confirm"
required
minlength="12"
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-gray-900"
placeholder="Repeat your password">
</div>
<button
type="submit"
class="w-full py-2 bg-gray-900 text-white rounded-md font-medium hover:bg-gray-800">
Create account
</button>
</form>
<p class="mt-6 text-center text-sm text-gray-600">
Already have an account? <a href="/login" class="font-medium text-gray-900 hover:underline">Sign in</a>
</p>
</section>
{% endblock %}

View File

@@ -0,0 +1,71 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-4xl mx-auto px-4 pt-12">
<div class="flex items-center justify-between mb-8">
<h1 class="text-2xl font-bold">Personal Access Tokens</h1>
<a href="/dashboard" class="text-sm text-gray-600 hover:text-gray-900">&larr; Dashboard</a>
</div>
{% if created_token %}
<div class="mb-6 p-4 bg-green-50 border border-green-200 rounded-md">
<p class="text-sm font-medium text-green-800 mb-2">Token created successfully. Copy it now - it won't be shown again.</p>
<div class="flex items-center gap-2">
<code class="flex-1 px-3 py-2 bg-white border border-green-300 rounded text-sm font-mono break-all">{{ created_token }}</code>
</div>
</div>
{% endif %}
<div class="mb-8 p-6 border border-gray-200 rounded-lg">
<h2 class="font-bold mb-4">Create new token</h2>
<form method="POST" action="/settings/tokens" class="flex gap-3">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<input
type="text"
name="name"
required
placeholder="Token name (e.g. CI/CD)"
class="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-gray-900">
<button
type="submit"
class="px-4 py-2 bg-gray-900 text-white rounded-md text-sm font-medium hover:bg-gray-800">
Create
</button>
</form>
</div>
{% if tokens %}
<div class="border border-gray-200 rounded-lg overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-gray-50">
<tr>
<th class="text-left px-6 py-3 font-medium">Name</th>
<th class="text-left px-6 py-3 font-medium">Created</th>
<th class="text-left px-6 py-3 font-medium">Last used</th>
<th class="text-left px-6 py-3 font-medium">Expires</th>
<th class="px-6 py-3"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
{% for token in tokens %}
<tr>
<td class="px-6 py-3 font-medium">{{ token.name }}</td>
<td class="px-6 py-3 text-gray-600">{{ token.created_at or "—" }}</td>
<td class="px-6 py-3 text-gray-600">{{ token.last_used or "Never" }}</td>
<td class="px-6 py-3 text-gray-600">{{ token.expires_at or "Never" }}</td>
<td class="px-6 py-3 text-right">
<form method="POST" action="/settings/tokens/{{ token.token_id }}/delete">
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<button type="submit" class="text-red-600 hover:text-red-800 text-sm">Revoke</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-sm text-gray-600">No tokens yet. Create one to use with the forest CLI.</p>
{% endif %}
</section>
{% endblock %}

View File

@@ -0,0 +1,55 @@
{% extends "base.html.jinja" %}
{% block content %}
<section class="max-w-4xl mx-auto px-4 pt-12">
<div class="flex items-center justify-between mb-8">
<div>
<a href="/orgs/{{ org_name }}/projects" class="text-sm text-gray-500 hover:text-gray-700">&larr; {{ org_name }}</a>
<h1 class="text-2xl font-bold mt-1">Usage &amp; Plan</h1>
</div>
</div>
<div class="p-6 border border-gray-200 rounded-lg mb-6">
<p class="text-sm text-gray-500">
Forage is in early access. Usage metering and billing are not yet active.
All accounts currently have free access to the platform.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
<div class="p-6 border border-gray-200 rounded-lg">
<h2 class="font-bold text-lg mb-2">Current Plan</h2>
<p class="text-2xl font-bold">Early Access</p>
<p class="text-sm text-gray-500 mt-1">Free during early access</p>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<h2 class="font-bold text-lg mb-2">Organisation</h2>
<p class="text-2xl font-bold">{{ org_name }}</p>
<p class="text-sm text-gray-500 mt-1">Role: {{ role }}</p>
</div>
</div>
<div class="p-6 border border-gray-200 rounded-lg">
<h2 class="font-bold text-lg mb-4">Resource Usage</h2>
<div class="space-y-3 text-sm">
<div class="flex justify-between">
<span class="text-gray-600">Projects</span>
<span>{{ project_count }} used</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Compute</span>
<span class="text-gray-400">Not yet metered</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">Storage</span>
<span class="text-gray-400">Not yet metered</span>
</div>
</div>
</div>
<div class="mt-8 text-center">
<a href="/pricing" class="text-sm text-gray-500 hover:text-gray-700">View full pricing &rarr;</a>
</div>
</section>
{% endblock %}