feat: safe area fix, delete from repo, add Mistral provider
Release APK / build (push) Has been cancelled
Release APK / build (push) Has been cancelled
- Wrap root layout with SafeAreaProvider, use useSafeAreaInsets in all screens so FAB/action bar/scroll content clear Android nav bar - Add deleteIncidentFromGit() (Gitea/GitHub/GitLab) — detail screen now offers Cancel / Local only / Local + Repo when incident was pushed - Add Mistral as AI provider (OpenAI-compat, api.mistral.ai/v1, mistral-small-latest default) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import { Pressable, Text } from "react-native";
|
|||||||
import { Stack, router } from "expo-router";
|
import { Stack, router } from "expo-router";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
||||||
|
import { SafeAreaProvider } from "react-native-safe-area-context";
|
||||||
import "../global.css";
|
import "../global.css";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
@@ -18,6 +19,7 @@ export default function RootLayout() {
|
|||||||
}, [ready, settings.onboardingDone]);
|
}, [ready, settings.onboardingDone]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<SafeAreaProvider>
|
||||||
<GestureHandlerRootView style={{ flex: 1, backgroundColor: Colors.bg }}>
|
<GestureHandlerRootView style={{ flex: 1, backgroundColor: Colors.bg }}>
|
||||||
<StatusBar style="light" />
|
<StatusBar style="light" />
|
||||||
<Stack
|
<Stack
|
||||||
@@ -49,5 +51,6 @@ export default function RootLayout() {
|
|||||||
<Stack.Screen name="onboarding" options={{ headerShown: false }} />
|
<Stack.Screen name="onboarding" options={{ headerShown: false }} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
|
</SafeAreaProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+43
-7
@@ -8,11 +8,13 @@ import {
|
|||||||
Share,
|
Share,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useLocalSearchParams, router, useNavigation } from "expo-router";
|
import { useLocalSearchParams, router, useNavigation } from "expo-router";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import * as Clipboard from "expo-clipboard";
|
import * as Clipboard from "expo-clipboard";
|
||||||
import * as SecureStore from "expo-secure-store";
|
import * as SecureStore from "expo-secure-store";
|
||||||
import { getIncidentById, updateIncident, deleteIncident, markIncidentPushed } from "@/lib/db";
|
import { getIncidentById, updateIncident, deleteIncident, markIncidentPushed } from "@/lib/db";
|
||||||
import { renderMarkdown } from "@/lib/markdown";
|
import { renderMarkdown } from "@/lib/markdown";
|
||||||
import { pushIncidentToGit, loadGitConfig } from "@/lib/git";
|
import { pushIncidentToGit, loadGitConfig, deleteIncidentFromGit } from "@/lib/git";
|
||||||
|
import type { GitConfig } from "@/lib/git";
|
||||||
import type { Incident } from "@/types/incident";
|
import type { Incident } from "@/types/incident";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
|
|
||||||
@@ -41,11 +43,14 @@ export default function IncidentDetailScreen() {
|
|||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
const [incident, setIncident] = useState<Incident | null>(null);
|
const [incident, setIncident] = useState<Incident | null>(null);
|
||||||
const [pushing, setPushing] = useState(false);
|
const [pushing, setPushing] = useState(false);
|
||||||
|
const [gitConfig, setGitConfig] = useState<GitConfig | null>(null);
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
getIncidentById(id).then((inc) => {
|
Promise.all([getIncidentById(id), loadGitConfig()]).then(([inc, config]) => {
|
||||||
setIncident(inc);
|
setIncident(inc);
|
||||||
|
setGitConfig(config);
|
||||||
if (inc) {
|
if (inc) {
|
||||||
navigation.setOptions({ title: inc.title || "Incident" });
|
navigation.setOptions({ title: inc.title || "Incident" });
|
||||||
}
|
}
|
||||||
@@ -55,6 +60,39 @@ export default function IncidentDetailScreen() {
|
|||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
if (!incident) return;
|
if (!incident) return;
|
||||||
|
|
||||||
|
const canDeleteFromRepo = !!incident.gitPushedAt && gitConfig !== null;
|
||||||
|
|
||||||
|
const doDelete = async (deleteFromRepo: boolean) => {
|
||||||
|
if (deleteFromRepo && gitConfig) {
|
||||||
|
const result = await deleteIncidentFromGit(incident, gitConfig);
|
||||||
|
if (!result.success) {
|
||||||
|
Alert.alert("Repo deletion failed", result.error ?? "Unknown error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await deleteIncident(incident.id);
|
||||||
|
router.replace("/");
|
||||||
|
};
|
||||||
|
|
||||||
|
if (canDeleteFromRepo) {
|
||||||
|
Alert.alert(
|
||||||
|
"Delete incident?",
|
||||||
|
"This incident was pushed to the repository. Do you also want to delete the file from the repo?",
|
||||||
|
[
|
||||||
|
{ text: "Cancel", style: "cancel" },
|
||||||
|
{
|
||||||
|
text: "Local only",
|
||||||
|
onPress: () => doDelete(false),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Local + Repo",
|
||||||
|
style: "destructive",
|
||||||
|
onPress: () => doDelete(true),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
"Delete incident?",
|
"Delete incident?",
|
||||||
"This action cannot be undone.",
|
"This action cannot be undone.",
|
||||||
@@ -63,14 +101,12 @@ export default function IncidentDetailScreen() {
|
|||||||
{
|
{
|
||||||
text: "Delete",
|
text: "Delete",
|
||||||
style: "destructive",
|
style: "destructive",
|
||||||
onPress: async () => {
|
onPress: () => doDelete(false),
|
||||||
await deleteIncident(incident.id);
|
|
||||||
router.replace("/");
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleResolve() {
|
async function handleResolve() {
|
||||||
if (!incident) return;
|
if (!incident) return;
|
||||||
@@ -125,7 +161,7 @@ export default function IncidentDetailScreen() {
|
|||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={{ flex: 1, backgroundColor: Colors.bg }}
|
style={{ flex: 1, backgroundColor: Colors.bg }}
|
||||||
contentContainerStyle={{ padding: 16, paddingBottom: 32 }}
|
contentContainerStyle={{ padding: 16, paddingBottom: 32 + insets.bottom }}
|
||||||
>
|
>
|
||||||
{/* Status badge */}
|
{/* Status badge */}
|
||||||
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8, marginBottom: 4 }}>
|
||||||
|
|||||||
+5
-2
@@ -8,6 +8,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { router, useFocusEffect } from "expo-router";
|
import { router, useFocusEffect } from "expo-router";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import * as SecureStore from "expo-secure-store";
|
import * as SecureStore from "expo-secure-store";
|
||||||
import { getIncidents, markIncidentPushed } from "@/lib/db";
|
import { getIncidents, markIncidentPushed } from "@/lib/db";
|
||||||
import { pushIncidentToGit, loadGitConfig } from "@/lib/git";
|
import { pushIncidentToGit, loadGitConfig } from "@/lib/git";
|
||||||
@@ -132,6 +133,7 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const gitConfigured = gitConfig !== null;
|
const gitConfigured = gitConfig !== null;
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: Colors.bg }}>
|
<View style={{ flex: 1, backgroundColor: Colors.bg }}>
|
||||||
@@ -141,7 +143,7 @@ export default function HomeScreen() {
|
|||||||
<FlatList
|
<FlatList
|
||||||
data={incidents}
|
data={incidents}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
contentContainerStyle={{ padding: 16, paddingBottom: 96 }}
|
contentContainerStyle={{ padding: 16, paddingBottom: 96 + insets.bottom }}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
@@ -275,7 +277,7 @@ export default function HomeScreen() {
|
|||||||
onPress={() => router.push("/new")}
|
onPress={() => router.push("/new")}
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
bottom: 24,
|
bottom: 24 + insets.bottom,
|
||||||
right: 24,
|
right: 24,
|
||||||
width: 56,
|
width: 56,
|
||||||
height: 56,
|
height: 56,
|
||||||
@@ -302,6 +304,7 @@ export default function HomeScreen() {
|
|||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: Colors.border,
|
borderTopColor: Colors.border,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
|
paddingBottom: 12 + insets.bottom,
|
||||||
gap: 10,
|
gap: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+3
-1
@@ -10,6 +10,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { router, useLocalSearchParams, useNavigation } from "expo-router";
|
import { router, useLocalSearchParams, useNavigation } from "expo-router";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import * as SecureStore from "expo-secure-store";
|
import * as SecureStore from "expo-secure-store";
|
||||||
import {
|
import {
|
||||||
createIncident,
|
createIncident,
|
||||||
@@ -55,6 +56,7 @@ type VoiceField = "title" | "service" | "symptom" | "rootCause" | "fix";
|
|||||||
export default function NewIncidentScreen() {
|
export default function NewIncidentScreen() {
|
||||||
const { editId } = useLocalSearchParams<{ editId?: string }>();
|
const { editId } = useLocalSearchParams<{ editId?: string }>();
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
|
|
||||||
const [form, setForm] = useState<IncidentDraft>({
|
const [form, setForm] = useState<IncidentDraft>({
|
||||||
title: "",
|
title: "",
|
||||||
@@ -250,7 +252,7 @@ export default function NewIncidentScreen() {
|
|||||||
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
behavior={Platform.OS === "ios" ? "padding" : undefined}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={{ padding: 16, paddingBottom: 32 }}
|
contentContainerStyle={{ padding: 16, paddingBottom: 32 + insets.bottom }}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
{labelRow("Title *", "title")}
|
{labelRow("Title *", "title")}
|
||||||
|
|||||||
+4
-1
@@ -7,6 +7,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Pressable,
|
Pressable,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import * as SecureStore from "expo-secure-store";
|
import * as SecureStore from "expo-secure-store";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
import { DEFAULT_TEMPLATE } from "@/lib/markdown";
|
import { DEFAULT_TEMPLATE } from "@/lib/markdown";
|
||||||
@@ -213,6 +214,7 @@ function Field({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsScreen() {
|
export default function SettingsScreen() {
|
||||||
|
const insets = useSafeAreaInsets();
|
||||||
const [inputMode, setInputMode] = useState<"voice" | "form">("form");
|
const [inputMode, setInputMode] = useState<"voice" | "form">("form");
|
||||||
const [homeView, setHomeView] = useState<"dashboard" | "capture">("dashboard");
|
const [homeView, setHomeView] = useState<"dashboard" | "capture">("dashboard");
|
||||||
const [aiEnabled, setAiEnabled] = useState(false);
|
const [aiEnabled, setAiEnabled] = useState(false);
|
||||||
@@ -315,7 +317,7 @@ export default function SettingsScreen() {
|
|||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={{ flex: 1, backgroundColor: Colors.bg }}
|
style={{ flex: 1, backgroundColor: Colors.bg }}
|
||||||
contentContainerStyle={{ padding: 16, paddingBottom: 48 }}
|
contentContainerStyle={{ padding: 16, paddingBottom: 48 + insets.bottom }}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
>
|
>
|
||||||
<Text style={SECTION_TITLE}>Preferences</Text>
|
<Text style={SECTION_TITLE}>Preferences</Text>
|
||||||
@@ -401,6 +403,7 @@ export default function SettingsScreen() {
|
|||||||
options={[
|
options={[
|
||||||
{ key: "anthropic", label: "Anthropic" },
|
{ key: "anthropic", label: "Anthropic" },
|
||||||
{ key: "openai", label: "OpenAI" },
|
{ key: "openai", label: "OpenAI" },
|
||||||
|
{ key: "mistral", label: "Mistral" },
|
||||||
{ key: "perplexity", label: "Perplexity" },
|
{ key: "perplexity", label: "Perplexity" },
|
||||||
{ key: "ollama", label: "Ollama" },
|
{ key: "ollama", label: "Ollama" },
|
||||||
{ key: "openrouter", label: "OpenRouter" },
|
{ key: "openrouter", label: "OpenRouter" },
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as SecureStore from "expo-secure-store";
|
|||||||
export type AIProvider =
|
export type AIProvider =
|
||||||
| "anthropic"
|
| "anthropic"
|
||||||
| "openai"
|
| "openai"
|
||||||
|
| "mistral"
|
||||||
| "perplexity"
|
| "perplexity"
|
||||||
| "ollama"
|
| "ollama"
|
||||||
| "openrouter"
|
| "openrouter"
|
||||||
@@ -18,6 +19,7 @@ const KEYS = {
|
|||||||
export const AI_PROVIDER_BASE_URLS: Record<AIProvider, string> = {
|
export const AI_PROVIDER_BASE_URLS: Record<AIProvider, string> = {
|
||||||
anthropic: "https://api.anthropic.com",
|
anthropic: "https://api.anthropic.com",
|
||||||
openai: "https://api.openai.com/v1",
|
openai: "https://api.openai.com/v1",
|
||||||
|
mistral: "https://api.mistral.ai/v1",
|
||||||
perplexity: "https://api.perplexity.ai",
|
perplexity: "https://api.perplexity.ai",
|
||||||
ollama: "http://localhost:11434/v1",
|
ollama: "http://localhost:11434/v1",
|
||||||
openrouter: "https://openrouter.ai/api/v1",
|
openrouter: "https://openrouter.ai/api/v1",
|
||||||
@@ -27,6 +29,7 @@ export const AI_PROVIDER_BASE_URLS: Record<AIProvider, string> = {
|
|||||||
export const AI_PROVIDER_MODEL_PLACEHOLDERS: Record<AIProvider, string> = {
|
export const AI_PROVIDER_MODEL_PLACEHOLDERS: Record<AIProvider, string> = {
|
||||||
anthropic: "claude-haiku-4-5-20251001",
|
anthropic: "claude-haiku-4-5-20251001",
|
||||||
openai: "gpt-4o-mini",
|
openai: "gpt-4o-mini",
|
||||||
|
mistral: "mistral-small-latest",
|
||||||
perplexity: "llama-3.1-sonar-small-128k-online",
|
perplexity: "llama-3.1-sonar-small-128k-online",
|
||||||
ollama: "llama3.2",
|
ollama: "llama3.2",
|
||||||
openrouter: "openai/gpt-4o-mini",
|
openrouter: "openai/gpt-4o-mini",
|
||||||
|
|||||||
+101
@@ -18,6 +18,11 @@ export interface PushResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeleteResult {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const TIMEOUT_MS = 15_000;
|
const TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
export async function loadGitConfig(): Promise<GitConfig | null> {
|
export async function loadGitConfig(): Promise<GitConfig | null> {
|
||||||
@@ -66,6 +71,20 @@ function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteIncidentFromGit(
|
||||||
|
incident: Incident,
|
||||||
|
config: GitConfig
|
||||||
|
): Promise<DeleteResult> {
|
||||||
|
switch (config.provider) {
|
||||||
|
case "gitea":
|
||||||
|
return deleteGitea(incident, config);
|
||||||
|
case "github":
|
||||||
|
return deleteGitHub(incident, config);
|
||||||
|
case "gitlab":
|
||||||
|
return deleteGitLab(incident, config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function pushIncidentToGit(
|
export async function pushIncidentToGit(
|
||||||
incident: Incident,
|
incident: Incident,
|
||||||
config: GitConfig,
|
config: GitConfig,
|
||||||
@@ -216,6 +235,88 @@ async function pushGitHub(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteGitea(incident: Incident, config: GitConfig): Promise<DeleteResult> {
|
||||||
|
if (!config.url) return { success: false, error: "Gitea instance URL is not set." };
|
||||||
|
const filepath = incidentToFilename(incident);
|
||||||
|
const apiBase = config.url.replace(/\/$/, "");
|
||||||
|
const endpoint = `${apiBase}/api/v1/repos/${config.owner}/${config.repo}/contents/${filepath}`;
|
||||||
|
try {
|
||||||
|
const existing = await fetchWithTimeout(endpoint, {
|
||||||
|
headers: { Authorization: `token ${config.token}` },
|
||||||
|
});
|
||||||
|
if (existing.status === 404) return { success: true };
|
||||||
|
if (!existing.ok) return { success: false, error: `GET ${existing.status}` };
|
||||||
|
const data = await existing.json() as { sha?: string };
|
||||||
|
if (!data.sha) return { success: false, error: "No SHA returned by API." };
|
||||||
|
|
||||||
|
const res = await fetchWithTimeout(endpoint, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { Authorization: `token ${config.token}`, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ message: `remove: ${incident.title}`, sha: data.sha, branch: "main" }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
return { success: false, error: `HTTP ${res.status}: ${body.slice(0, 200)}` };
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === "AbortError") return { success: false, error: `Request timed out after ${TIMEOUT_MS / 1000}s.` };
|
||||||
|
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteGitHub(incident: Incident, config: GitConfig): Promise<DeleteResult> {
|
||||||
|
const filepath = incidentToFilename(incident);
|
||||||
|
const endpoint = `https://api.github.com/repos/${config.owner}/${config.repo}/contents/${filepath}`;
|
||||||
|
try {
|
||||||
|
const existing = await fetchWithTimeout(endpoint, {
|
||||||
|
headers: { Authorization: `Bearer ${config.token}`, Accept: "application/vnd.github+json" },
|
||||||
|
});
|
||||||
|
if (existing.status === 404) return { success: true };
|
||||||
|
if (!existing.ok) return { success: false, error: `GET ${existing.status}` };
|
||||||
|
const data = await existing.json() as { sha?: string };
|
||||||
|
if (!data.sha) return { success: false, error: "No SHA returned by API." };
|
||||||
|
|
||||||
|
const res = await fetchWithTimeout(endpoint, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { Authorization: `Bearer ${config.token}`, Accept: "application/vnd.github+json", "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ message: `remove: ${incident.title}`, sha: data.sha }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
return { success: false, error: `HTTP ${res.status}: ${body.slice(0, 200)}` };
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === "AbortError") return { success: false, error: `Request timed out after ${TIMEOUT_MS / 1000}s.` };
|
||||||
|
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteGitLab(incident: Incident, config: GitConfig): Promise<DeleteResult> {
|
||||||
|
const filepath = incidentToFilename(incident);
|
||||||
|
const apiBase = (config.url ?? "https://gitlab.com").replace(/\/$/, "");
|
||||||
|
const projectId = encodeURIComponent(`${config.owner}/${config.repo}`);
|
||||||
|
const encodedPath = encodeURIComponent(filepath);
|
||||||
|
const endpoint = `${apiBase}/api/v4/projects/${projectId}/repository/files/${encodedPath}`;
|
||||||
|
try {
|
||||||
|
const res = await fetchWithTimeout(endpoint, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: { "PRIVATE-TOKEN": config.token, "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ branch: "main", commit_message: `remove: ${incident.title}` }),
|
||||||
|
});
|
||||||
|
if (res.status === 404) return { success: true };
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.text();
|
||||||
|
return { success: false, error: `HTTP ${res.status}: ${body.slice(0, 200)}` };
|
||||||
|
}
|
||||||
|
return { success: true };
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Error && e.name === "AbortError") return { success: false, error: `Request timed out after ${TIMEOUT_MS / 1000}s.` };
|
||||||
|
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function pushGitLab(
|
async function pushGitLab(
|
||||||
incident: Incident,
|
incident: Incident,
|
||||||
config: GitConfig,
|
config: GitConfig,
|
||||||
|
|||||||
Reference in New Issue
Block a user