91
templates/base.html.jinja
Normal file
91
templates/base.html.jinja
Normal 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">
|
||||
© 2026 Forage. Built with Forest.
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
19
templates/docker-compose.yaml
Normal file
19
templates/docker-compose.yaml
Normal 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:
|
||||
57
templates/forage-server.Dockerfile
Normal file
57
templates/forage-server.Dockerfile
Normal 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"]
|
||||
28
templates/pages/components.html.jinja
Normal file
28
templates/pages/components.html.jinja
Normal 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 %}
|
||||
57
templates/pages/dashboard.html.jinja
Normal file
57
templates/pages/dashboard.html.jinja
Normal 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 %}
|
||||
10
templates/pages/error.html.jinja
Normal file
10
templates/pages/error.html.jinja
Normal 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">← Back to home</a>
|
||||
</section>
|
||||
{% endblock %}
|
||||
104
templates/pages/landing.html.jinja
Normal file
104
templates/pages/landing.html.jinja
Normal 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 %}
|
||||
48
templates/pages/login.html.jinja
Normal file
48
templates/pages/login.html.jinja
Normal 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 %}
|
||||
26
templates/pages/onboarding.html.jinja
Normal file
26
templates/pages/onboarding.html.jinja
Normal 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 →</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
123
templates/pages/pricing.html.jinja
Normal file
123
templates/pages/pricing.html.jinja
Normal 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 %}
|
||||
40
templates/pages/project_detail.html.jinja
Normal file
40
templates/pages/project_detail.html.jinja
Normal 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">← {{ 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 %}
|
||||
24
templates/pages/projects.html.jinja
Normal file
24
templates/pages/projects.html.jinja
Normal 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 %}
|
||||
75
templates/pages/signup.html.jinja
Normal file
75
templates/pages/signup.html.jinja
Normal 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 %}
|
||||
71
templates/pages/tokens.html.jinja
Normal file
71
templates/pages/tokens.html.jinja
Normal 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">← 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 %}
|
||||
55
templates/pages/usage.html.jinja
Normal file
55
templates/pages/usage.html.jinja
Normal 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">← {{ org_name }}</a>
|
||||
<h1 class="text-2xl font-bold mt-1">Usage & 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 →</a>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user