Merge pull request 'fix(ui): use app icon in header' (#3) from fix/use-real-app-icon into main
Deploy / Build (push) Successful in 31s
Deploy / Build and Push Docker Image (push) Successful in 1m34s

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-07-26 17:11:36 +02:00
10 changed files with 67 additions and 13 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
## UI Related ## UI Related
- [ ] Fix Theming to be more professional (use frontend themeing skill & make it look like a professional product) - [ ] Fix Theming to be more professional (use frontend themeing skill & make it look like a professional product)
- [x] replace emoji in the top left with the actual icon
## Future BS ## Future BS
- [ ] CPU, MEM, NET, DISK sentinal first-installed on EC2s to expose a backend for the user's ui to hit, THROUGH A BACKEND PROXY ROUTE so we can cache and rate limit the requests. This will allow for a better dashboard and better stats for the user to see. We'll use an obscure port for the sentinal so that we dont hit any other services the user may have. Sentinal is our own little C program exposing the stats we need, seperate repo for that thing tho! - [ ] CPU, MEM, NET, DISK sentinal first-installed on EC2s to expose a backend for the user's ui to hit, THROUGH A BACKEND PROXY ROUTE so we can cache and rate limit the requests. This will allow for a better dashboard and better stats for the user to see. We'll use an obscure port for the sentinal so that we dont hit any other services the user may have. Sentinal is our own little C program exposing the stats we need, seperate repo for that thing tho!
+2 -1
View File
@@ -18,7 +18,7 @@ import { runWebhookReconciliation } from "./services/webhookReconcile";
import { loginHandler, logoutHandler, meHandler, setupStatusHandler, firstUserHandler } from "./routes/auth"; import { loginHandler, logoutHandler, meHandler, setupStatusHandler, firstUserHandler } from "./routes/auth";
import { webhookHandler } from "./routes/webhook"; import { webhookHandler } from "./routes/webhook";
import { getUserSettings, updateUsername, updatePassword, updateGitea, updateAws, getWebhookSecret, regenerateWebhookSecret } from "./routes/api/user"; import { getUserSettings, updateUsername, updatePassword, updateGitea, updateAws, skipSetupWizard, getWebhookSecret, regenerateWebhookSecret } from "./routes/api/user";
import { listRepos, saveRepoConfig, toggleRepoEnabled, getRepoConfig } from "./routes/api/repos"; import { listRepos, saveRepoConfig, toggleRepoEnabled, getRepoConfig } from "./routes/api/repos";
import { listPreviews, getPreview, stopPreviewRoute, rebuildPreviewRoute, previewLogsWs, previewAppLogsWs } from "./routes/api/previews"; import { listPreviews, getPreview, stopPreviewRoute, rebuildPreviewRoute, previewLogsWs, previewAppLogsWs } from "./routes/api/previews";
import { getStats } from "./routes/api/stats"; import { getStats } from "./routes/api/stats";
@@ -66,6 +66,7 @@ server.path("/", (path) => path
.http("PATCH", "/api/user/password", (http) => http.onRequest(updatePassword)) .http("PATCH", "/api/user/password", (http) => http.onRequest(updatePassword))
.http("PUT", "/api/user/gitea", (http) => http.onRequest(updateGitea)) .http("PUT", "/api/user/gitea", (http) => http.onRequest(updateGitea))
.http("PUT", "/api/user/aws", (http) => http.onRequest(updateAws)) .http("PUT", "/api/user/aws", (http) => http.onRequest(updateAws))
.http("POST", "/api/user/setup/skip", (http) => http.onRequest(skipSetupWizard))
.http("GET", "/api/user/webhook-secret", (http) => http.onRequest(getWebhookSecret)) .http("GET", "/api/user/webhook-secret", (http) => http.onRequest(getWebhookSecret))
.http("POST", "/api/user/webhook-secret/regenerate", (http) => http.onRequest(regenerateWebhookSecret)) .http("POST", "/api/user/webhook-secret/regenerate", (http) => http.onRequest(regenerateWebhookSecret))
); );
+12
View File
@@ -181,6 +181,18 @@ export async function updateAws(ctr: any) {
return makeResponse({ ctr, content: { code: 200, message: `Connected as ${validation.arn}`, data: { arn: validation.arn } } }); return makeResponse({ ctr, content: { code: 200, message: `Connected as ${validation.arn}`, data: { arn: validation.arn } } });
} }
export async function skipSetupWizard(ctr: any) {
const user = requireAuth(ctr);
if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } });
await prisma.user.update({
where: { id: user.id },
data: { setupSkipped: true },
});
return makeResponse({ ctr, content: { code: 200, message: "Setup wizard skipped" } });
}
export async function getWebhookSecret(ctr: any) { export async function getWebhookSecret(ctr: any) {
const user = requireAuth(ctr); const user = requireAuth(ctr);
if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } }); if (!user) return makeResponse({ ctr, content: { code: 401, message: ERROR_MESSAGES.UNAUTHORIZED.message } });
+13 -1
View File
@@ -10,6 +10,17 @@ const log = createLogger("AUTH_ROUTE");
const COOKIE_NAME = "pp_session"; const COOKIE_NAME = "pp_session";
const COOKIE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000; const COOKIE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
function isSetupComplete(user: {
setupSkipped: boolean;
giteaInstanceUrl: string | null;
giteaPAT: string | null;
awsAccessKeyId: string | null;
awsSecretAccessKey: string | null;
awsRegion: string | null;
}) {
return user.setupSkipped || !!(user.giteaInstanceUrl && user.giteaPAT && user.awsAccessKeyId && user.awsSecretAccessKey && user.awsRegion);
}
export async function loginHandler(ctr: any) { export async function loginHandler(ctr: any) {
let body: any; let body: any;
try { try {
@@ -82,7 +93,8 @@ export async function meHandler(ctr: any) {
awsAccessKeyId: user.awsAccessKeyId ? "****" : null, awsAccessKeyId: user.awsAccessKeyId ? "****" : null,
awsRegion: user.awsRegion, awsRegion: user.awsRegion,
awsConfigured: !!(user.awsAccessKeyId && user.awsSecretAccessKey && user.awsRegion), awsConfigured: !!(user.awsAccessKeyId && user.awsSecretAccessKey && user.awsRegion),
setupComplete: !!(user.giteaInstanceUrl && user.giteaPAT && user.awsAccessKeyId && user.awsSecretAccessKey && user.awsRegion), setupSkipped: user.setupSkipped,
setupComplete: isSetupComplete(user),
}, },
}, },
}); });
+2 -2
View File
@@ -3,7 +3,7 @@ import { Link, useLocation, useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth"; import { useAuth } from "../hooks/useAuth";
import { ThemePreference, useTheme } from "../hooks/useTheme"; import { ThemePreference, useTheme } from "../hooks/useTheme";
import { api } from "../services/api"; import { api } from "../services/api";
import { Rocket, Sun, Moon, Monitor } from "lucide-react"; import { Sun, Moon, Monitor } from "lucide-react";
const themeOptions: { value: ThemePreference; label: string; Icon: typeof Sun }[] = [ const themeOptions: { value: ThemePreference; label: string; Icon: typeof Sun }[] = [
{ value: "light", label: "Light", Icon: Sun }, { value: "light", label: "Light", Icon: Sun },
@@ -42,7 +42,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
<div className="max-w-7xl mx-auto px-4 h-14 flex items-center justify-between gap-4"> <div className="max-w-7xl mx-auto px-4 h-14 flex items-center justify-between gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Link to="/" className="font-bold text-lg text-blue-600 dark:text-blue-400 hover:opacity-80 inline-flex items-center gap-1.5"> <Link to="/" className="font-bold text-lg text-blue-600 dark:text-blue-400 hover:opacity-80 inline-flex items-center gap-1.5">
<Rocket size={20} /> PR Previews <img src="/icon.png" alt="" className="h-5 w-5 shrink-0 rounded" aria-hidden="true" /> PR Previews
</Link> </Link>
</div> </div>
+1
View File
@@ -6,6 +6,7 @@ export interface AuthUser {
username: string; username: string;
isAdmin: boolean; isAdmin: boolean;
isFounder: boolean; isFounder: boolean;
setupSkipped: boolean;
setupComplete: boolean; setupComplete: boolean;
giteaUsername: string | null; giteaUsername: string | null;
giteaInstanceUrl: string | null; giteaInstanceUrl: string | null;
+33 -8
View File
@@ -44,7 +44,32 @@ export function SetupWizard() {
}, [step, user?.id]); }, [step, user?.id]);
const next = () => setStep(s => Math.min(s + 1, STEPS.length - 1)); const next = () => setStep(s => Math.min(s + 1, STEPS.length - 1));
const skip = () => navigate("/"); const skip = async () => {
setSaving(true);
const res = await api.user.skipSetup();
setSaving(false);
if (res.ok) {
await refresh();
navigate("/");
} else {
toast.error(res.message || "Failed to skip setup");
}
};
const leaveWizard = async (path: string) => {
if (user?.setupComplete) {
navigate(path);
return;
}
setSaving(true);
const res = await api.user.skipSetup();
setSaving(false);
if (res.ok) {
await refresh();
navigate(path);
} else {
toast.error(res.message || "Failed to finish setup");
}
};
const handleGiteaNext = async () => { const handleGiteaNext = async () => {
setSaving(true); setSaving(true);
@@ -112,8 +137,8 @@ export function SetupWizard() {
<button onClick={next} className="inline-flex items-center gap-1 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"> <button onClick={next} className="inline-flex items-center gap-1 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors">
Get Started <ArrowRight size={16} /> Get Started <ArrowRight size={16} />
</button> </button>
<button onClick={skip} className="px-4 py-2.5 text-sm text-gray-500 dark:text-slate-400 hover:underline"> <button onClick={skip} disabled={saving} className="px-4 py-2.5 text-sm text-gray-500 dark:text-slate-400 hover:underline disabled:opacity-50">
Skip wizard {saving ? "Skipping..." : "Skip wizard"}
</button> </button>
</div> </div>
</div> </div>
@@ -211,10 +236,10 @@ export function SetupWizard() {
Go to the Repos page to enable previews for a repository. PP will automatically register the webhook. Go to the Repos page to enable previews for a repository. PP will automatically register the webhook.
</p> </p>
<div className="flex gap-3"> <div className="flex gap-3">
<a href="/repos" <button onClick={() => leaveWizard("/repos")} disabled={saving}
className="flex-1 inline-flex items-center justify-center gap-1 py-2.5 text-center bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"> className="flex-1 inline-flex items-center justify-center gap-1 py-2.5 text-center bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors disabled:opacity-50">
Go to Repos <ArrowRight size={16} /> Go to Repos <ArrowRight size={16} />
</a> </button>
<button onClick={next} className="text-sm text-gray-500 dark:text-slate-400 hover:underline px-2">Skip</button> <button onClick={next} className="text-sm text-gray-500 dark:text-slate-400 hover:underline px-2">Skip</button>
</div> </div>
</div> </div>
@@ -227,8 +252,8 @@ export function SetupWizard() {
<p className="text-gray-600 dark:text-slate-300 text-sm"> <p className="text-gray-600 dark:text-slate-300 text-sm">
Open a pull request on an enabled repo and PP will provision a preview automatically. Open a pull request on an enabled repo and PP will provision a preview automatically.
</p> </p>
<button onClick={() => navigate("/")} className="inline-flex items-center gap-1 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors"> <button onClick={() => leaveWizard("/")} disabled={saving} className="inline-flex items-center gap-1 px-6 py-2.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium transition-colors disabled:opacity-50">
Go to Dashboard <ArrowRight size={16} /> {saving ? "Finishing..." : <span className="inline-flex items-center gap-1">Go to Dashboard <ArrowRight size={16} /></span>}
</button> </button>
</div> </div>
)} )}
+1
View File
@@ -37,6 +37,7 @@ export const api = {
request("PUT", "/user/gitea", data), request("PUT", "/user/gitea", data),
updateAws: (data: { awsAccessKeyId: string; awsSecretAccessKey: string; awsRegion: string }) => updateAws: (data: { awsAccessKeyId: string; awsSecretAccessKey: string; awsRegion: string }) =>
request("PUT", "/user/aws", data), request("PUT", "/user/aws", data),
skipSetup: () => request("POST", "/user/setup/skip"),
getWebhookSecret: () => request("GET", "/user/webhook-secret"), getWebhookSecret: () => request("GET", "/user/webhook-secret"),
regenerateWebhookSecret: () => request("POST", "/user/webhook-secret/regenerate"), regenerateWebhookSecret: () => request("POST", "/user/webhook-secret/regenerate"),
}, },
@@ -0,0 +1 @@
ALTER TABLE "User" ADD COLUMN "setupSkipped" BOOLEAN NOT NULL DEFAULT false;
+1
View File
@@ -36,6 +36,7 @@ model User {
passwordHash String @db.VarChar(256) passwordHash String @db.VarChar(256)
isAdmin Boolean @default(false) isAdmin Boolean @default(false)
isFounder Boolean @default(false) isFounder Boolean @default(false)
setupSkipped Boolean @default(false)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt