fee283b5d6
Release APK / build (push) Has been cancelled
- Replace app icon with new Charbon·Lime brand assets (icon.png + adaptive-icon.png) - Apply design tokens to theme.ts: bg #1B2433, primary/success #88D656 (lime), text1 #EEF2F5 (cream) - Add ⚙ gear button (lime) in HomeScreen header → navigates to /settings - Expand settings: git provider selector (Gitea / GitHub / GitLab) with per-provider fields - Add lib/git.ts: unified pushIncidentToGit() for Gitea, GitHub (PUT), and GitLab APIs - Update incident detail screen to use lib/git.ts and dynamic provider routing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
298 lines
8.6 KiB
TypeScript
298 lines
8.6 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
ScrollView,
|
|
Pressable,
|
|
Alert,
|
|
Share,
|
|
} from "react-native";
|
|
import { useLocalSearchParams, router, useNavigation } from "expo-router";
|
|
import * as Clipboard from "expo-clipboard";
|
|
import * as SecureStore from "expo-secure-store";
|
|
import { getIncidentById, updateIncident } from "@/lib/db";
|
|
import { renderMarkdown } from "@/lib/markdown";
|
|
import { pushIncidentToGit } from "@/lib/git";
|
|
import type { GitProvider } from "@/lib/git";
|
|
import type { Incident } from "@/types/incident";
|
|
import { Colors } from "@/constants/theme";
|
|
|
|
const SECTION_LABEL = {
|
|
color: Colors.text2,
|
|
fontSize: 11,
|
|
fontWeight: "700" as const,
|
|
textTransform: "uppercase" as const,
|
|
letterSpacing: 0.8,
|
|
marginBottom: 6,
|
|
marginTop: 20,
|
|
} as const;
|
|
|
|
const SECTION_CONTENT = {
|
|
color: Colors.text1,
|
|
fontSize: 14,
|
|
fontFamily: "monospace",
|
|
backgroundColor: Colors.surface,
|
|
borderRadius: 8,
|
|
padding: 12,
|
|
lineHeight: 20,
|
|
} as const;
|
|
|
|
export default function IncidentDetailScreen() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const navigation = useNavigation();
|
|
const [incident, setIncident] = useState<Incident | null>(null);
|
|
const [pushing, setPushing] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (id) {
|
|
getIncidentById(id).then((inc) => {
|
|
setIncident(inc);
|
|
if (inc) {
|
|
navigation.setOptions({ title: inc.title || "Incident" });
|
|
}
|
|
});
|
|
}
|
|
}, [id]);
|
|
|
|
async function handleResolve() {
|
|
if (!incident) return;
|
|
await updateIncident(incident.id, { status: "resolved" });
|
|
setIncident({ ...incident, status: "resolved" });
|
|
}
|
|
|
|
async function handleGitPush() {
|
|
if (!incident) return;
|
|
const [provider, template, giteaUrl, giteaToken, giteaOwner, giteaRepo, githubToken, githubOwner, githubRepo, gitlabUrl, gitlabToken, gitlabOwner, gitlabRepo] =
|
|
await Promise.all([
|
|
SecureStore.getItemAsync("git_provider"),
|
|
SecureStore.getItemAsync("md_template"),
|
|
SecureStore.getItemAsync("gitea_url"),
|
|
SecureStore.getItemAsync("gitea_token"),
|
|
SecureStore.getItemAsync("gitea_owner"),
|
|
SecureStore.getItemAsync("gitea_repo"),
|
|
SecureStore.getItemAsync("github_token"),
|
|
SecureStore.getItemAsync("github_owner"),
|
|
SecureStore.getItemAsync("github_repo"),
|
|
SecureStore.getItemAsync("gitlab_url"),
|
|
SecureStore.getItemAsync("gitlab_token"),
|
|
SecureStore.getItemAsync("gitlab_owner"),
|
|
SecureStore.getItemAsync("gitlab_repo"),
|
|
]);
|
|
|
|
const p = (provider ?? "gitea") as GitProvider;
|
|
|
|
let url: string | undefined;
|
|
let token: string | null;
|
|
let owner: string | null;
|
|
let repo: string | null;
|
|
|
|
if (p === "gitea") {
|
|
url = giteaUrl ?? undefined;
|
|
token = giteaToken;
|
|
owner = giteaOwner;
|
|
repo = giteaRepo;
|
|
} else if (p === "github") {
|
|
token = githubToken;
|
|
owner = githubOwner;
|
|
repo = githubRepo;
|
|
} else {
|
|
url = gitlabUrl ?? undefined;
|
|
token = gitlabToken;
|
|
owner = gitlabOwner;
|
|
repo = gitlabRepo;
|
|
}
|
|
|
|
if (!token || !owner || !repo) {
|
|
Alert.alert("Git not configured", `Set up ${p.charAt(0).toUpperCase() + p.slice(1)} in Settings first.`);
|
|
return;
|
|
}
|
|
|
|
setPushing(true);
|
|
const result = await pushIncidentToGit(
|
|
incident,
|
|
{ provider: p, url, token, owner, repo },
|
|
template ?? undefined
|
|
);
|
|
setPushing(false);
|
|
if (result.success) {
|
|
Alert.alert("Pushed", result.url ?? "Push successful.");
|
|
} else {
|
|
Alert.alert("Push failed", result.error ?? "Unknown error");
|
|
}
|
|
}
|
|
|
|
async function handleExport() {
|
|
if (!incident) return;
|
|
const md = renderMarkdown(incident);
|
|
try {
|
|
await Share.share({ message: md, title: incident.title });
|
|
} catch {
|
|
await Clipboard.setStringAsync(md);
|
|
Alert.alert("Copied", "Markdown copied to clipboard.");
|
|
}
|
|
}
|
|
|
|
if (!incident) {
|
|
return (
|
|
<View
|
|
style={{ flex: 1, backgroundColor: Colors.bg, alignItems: "center", justifyContent: "center" }}
|
|
>
|
|
<Text style={{ color: Colors.textDim }}>Loading…</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ScrollView
|
|
style={{ flex: 1, backgroundColor: Colors.bg }}
|
|
contentContainerStyle={{ padding: 16, paddingBottom: 32 }}
|
|
>
|
|
{/* Status badge */}
|
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
|
<View
|
|
style={{
|
|
backgroundColor:
|
|
incident.status === "open" ? "#f59e0b20" : "#88D65620",
|
|
borderRadius: 6,
|
|
paddingHorizontal: 10,
|
|
paddingVertical: 4,
|
|
}}
|
|
>
|
|
<Text
|
|
style={{
|
|
color: incident.status === "open" ? Colors.warning : Colors.success,
|
|
fontSize: 12,
|
|
fontWeight: "700",
|
|
textTransform: "uppercase",
|
|
}}
|
|
>
|
|
{incident.status}
|
|
</Text>
|
|
</View>
|
|
{incident.service ? (
|
|
<Text style={{ color: Colors.textDim, fontSize: 12 }}>
|
|
{incident.service}
|
|
</Text>
|
|
) : null}
|
|
<Text style={{ color: Colors.textDim, fontSize: 12 }}>
|
|
{incident.createdAt.slice(0, 16).replace("T", " ")}
|
|
</Text>
|
|
</View>
|
|
|
|
<Text
|
|
style={{
|
|
color: Colors.text1,
|
|
fontSize: 22,
|
|
fontWeight: "700",
|
|
marginBottom: 4,
|
|
}}
|
|
>
|
|
{incident.title || "Untitled"}
|
|
</Text>
|
|
|
|
{incident.tags.length > 0 && (
|
|
<View style={{ flexDirection: "row", flexWrap: "wrap", gap: 6, marginTop: 8 }}>
|
|
{incident.tags.map((tag) => (
|
|
<View
|
|
key={tag}
|
|
style={{
|
|
backgroundColor: Colors.surface2,
|
|
borderRadius: 4,
|
|
paddingHorizontal: 8,
|
|
paddingVertical: 3,
|
|
}}
|
|
>
|
|
<Text style={{ color: Colors.text2, fontSize: 12 }}>{tag}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
|
|
<Text style={SECTION_LABEL}>Symptom</Text>
|
|
<Text style={SECTION_CONTENT}>{incident.symptom || "—"}</Text>
|
|
|
|
<Text style={SECTION_LABEL}>Root Cause</Text>
|
|
<Text style={SECTION_CONTENT}>{incident.rootCause || "—"}</Text>
|
|
|
|
<Text style={SECTION_LABEL}>Fix Applied</Text>
|
|
<Text style={SECTION_CONTENT}>{incident.fix || "—"}</Text>
|
|
|
|
{/* Actions */}
|
|
<View style={{ gap: 10, marginTop: 28 }}>
|
|
{incident.status === "open" && (
|
|
<Pressable
|
|
onPress={handleResolve}
|
|
style={{
|
|
backgroundColor: "#88D65620",
|
|
borderWidth: 1,
|
|
borderColor: Colors.success,
|
|
borderRadius: 10,
|
|
padding: 14,
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<Text style={{ color: Colors.success, fontWeight: "700", fontSize: 15 }}>
|
|
Mark as Resolved
|
|
</Text>
|
|
</Pressable>
|
|
)}
|
|
|
|
<Pressable
|
|
onPress={handleExport}
|
|
style={{
|
|
backgroundColor: Colors.surface,
|
|
borderWidth: 1,
|
|
borderColor: Colors.border,
|
|
borderRadius: 10,
|
|
padding: 14,
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<Text style={{ color: Colors.text1, fontWeight: "600", fontSize: 15 }}>
|
|
Export Markdown
|
|
</Text>
|
|
</Pressable>
|
|
|
|
<Pressable
|
|
onPress={handleGitPush}
|
|
disabled={pushing}
|
|
style={{
|
|
backgroundColor: pushing ? Colors.surface2 : Colors.surface,
|
|
borderWidth: 1,
|
|
borderColor: pushing ? Colors.border : Colors.primary,
|
|
borderRadius: 10,
|
|
padding: 14,
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<Text
|
|
style={{
|
|
color: pushing ? Colors.textDim : Colors.primary,
|
|
fontWeight: "600",
|
|
fontSize: 15,
|
|
}}
|
|
>
|
|
{pushing ? "Pushing…" : "Push to Git"}
|
|
</Text>
|
|
</Pressable>
|
|
|
|
<Pressable
|
|
onPress={() => router.push({ pathname: "/new", params: { editId: incident.id } })}
|
|
style={{
|
|
backgroundColor: Colors.surface,
|
|
borderWidth: 1,
|
|
borderColor: Colors.border,
|
|
borderRadius: 10,
|
|
padding: 14,
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<Text style={{ color: Colors.text2, fontWeight: "600", fontSize: 15 }}>
|
|
Edit
|
|
</Text>
|
|
</Pressable>
|
|
</View>
|
|
</ScrollView>
|
|
);
|
|
}
|