Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fee283b5d6 | |||
| 67c132d88b |
@@ -5,12 +5,13 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
"backgroundColor": "#0f1117",
|
"icon": "./assets/icon.png",
|
||||||
|
"backgroundColor": "#1B2433",
|
||||||
"scheme": "sheethappens",
|
"scheme": "sheethappens",
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"foregroundImage": "./assets/adaptive-icon.png",
|
"foregroundImage": "./assets/adaptive-icon.png",
|
||||||
"backgroundColor": "#0f1117"
|
"backgroundColor": "#1B2433"
|
||||||
},
|
},
|
||||||
"package": "fr.gyozamancave.sheethappens",
|
"package": "fr.gyozamancave.sheethappens",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
|||||||
+15
-1
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
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";
|
||||||
@@ -28,7 +29,20 @@ export default function RootLayout() {
|
|||||||
animation: "slide_from_right",
|
animation: "slide_from_right",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack.Screen name="index" options={{ title: "SheetHappens" }} />
|
<Stack.Screen
|
||||||
|
name="index"
|
||||||
|
options={{
|
||||||
|
title: "SheetHappens",
|
||||||
|
headerRight: () => (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => router.push("/settings")}
|
||||||
|
style={{ paddingLeft: 12, paddingRight: 4, paddingVertical: 6 }}
|
||||||
|
>
|
||||||
|
<Text style={{ color: Colors.primary, fontSize: 22, lineHeight: 26 }}>⚙</Text>
|
||||||
|
</Pressable>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Stack.Screen name="new" options={{ title: "New Incident", presentation: "modal" }} />
|
<Stack.Screen name="new" options={{ title: "New Incident", presentation: "modal" }} />
|
||||||
<Stack.Screen name="incident/[id]" options={{ title: "Incident" }} />
|
<Stack.Screen name="incident/[id]" options={{ title: "Incident" }} />
|
||||||
<Stack.Screen name="settings" options={{ title: "Settings" }} />
|
<Stack.Screen name="settings" options={{ title: "Settings" }} />
|
||||||
|
|||||||
+48
-13
@@ -12,7 +12,8 @@ import * as Clipboard from "expo-clipboard";
|
|||||||
import * as SecureStore from "expo-secure-store";
|
import * as SecureStore from "expo-secure-store";
|
||||||
import { getIncidentById, updateIncident } from "@/lib/db";
|
import { getIncidentById, updateIncident } from "@/lib/db";
|
||||||
import { renderMarkdown } from "@/lib/markdown";
|
import { renderMarkdown } from "@/lib/markdown";
|
||||||
import { pushIncidentToGitea } from "@/lib/gitea";
|
import { pushIncidentToGit } from "@/lib/git";
|
||||||
|
import type { GitProvider } 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";
|
||||||
|
|
||||||
@@ -59,28 +60,62 @@ export default function IncidentDetailScreen() {
|
|||||||
setIncident({ ...incident, status: "resolved" });
|
setIncident({ ...incident, status: "resolved" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleGiteaPush() {
|
async function handleGitPush() {
|
||||||
if (!incident) return;
|
if (!incident) return;
|
||||||
const [url, token, owner, repo, template] = await Promise.all([
|
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_url"),
|
||||||
SecureStore.getItemAsync("gitea_token"),
|
SecureStore.getItemAsync("gitea_token"),
|
||||||
SecureStore.getItemAsync("gitea_owner"),
|
SecureStore.getItemAsync("gitea_owner"),
|
||||||
SecureStore.getItemAsync("gitea_repo"),
|
SecureStore.getItemAsync("gitea_repo"),
|
||||||
SecureStore.getItemAsync("md_template"),
|
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"),
|
||||||
]);
|
]);
|
||||||
if (!url || !token || !owner || !repo) {
|
|
||||||
Alert.alert("Gitea not configured", "Set up Gitea in Settings first.");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setPushing(true);
|
setPushing(true);
|
||||||
const result = await pushIncidentToGitea(
|
const result = await pushIncidentToGit(
|
||||||
incident,
|
incident,
|
||||||
{ url, token, owner, repo },
|
{ provider: p, url, token, owner, repo },
|
||||||
template ?? undefined
|
template ?? undefined
|
||||||
);
|
);
|
||||||
setPushing(false);
|
setPushing(false);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
Alert.alert("Pushed", result.url ? `${result.url}` : "Push successful.");
|
Alert.alert("Pushed", result.url ?? "Push successful.");
|
||||||
} else {
|
} else {
|
||||||
Alert.alert("Push failed", result.error ?? "Unknown error");
|
Alert.alert("Push failed", result.error ?? "Unknown error");
|
||||||
}
|
}
|
||||||
@@ -117,7 +152,7 @@ export default function IncidentDetailScreen() {
|
|||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
incident.status === "open" ? "#f59e0b20" : "#22c55e20",
|
incident.status === "open" ? "#f59e0b20" : "#88D65620",
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
@@ -188,7 +223,7 @@ export default function IncidentDetailScreen() {
|
|||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleResolve}
|
onPress={handleResolve}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#22c55e20",
|
backgroundColor: "#88D65620",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: Colors.success,
|
borderColor: Colors.success,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
@@ -219,7 +254,7 @@ export default function IncidentDetailScreen() {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={handleGiteaPush}
|
onPress={handleGitPush}
|
||||||
disabled={pushing}
|
disabled={pushing}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: pushing ? Colors.surface2 : Colors.surface,
|
backgroundColor: pushing ? Colors.surface2 : Colors.surface,
|
||||||
@@ -237,7 +272,7 @@ export default function IncidentDetailScreen() {
|
|||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{pushing ? "Pushing…" : "Push to Gitea"}
|
{pushing ? "Pushing…" : "Push to Git"}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
Pressable,
|
Pressable,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { router } from "expo-router";
|
import { router, useFocusEffect } from "expo-router";
|
||||||
import { getIncidents } from "@/lib/db";
|
import { getIncidents } from "@/lib/db";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
import type { Incident } from "@/types/incident";
|
import type { Incident } from "@/types/incident";
|
||||||
@@ -26,9 +26,11 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
}, [ready, settings.homeView]);
|
}, [ready, settings.homeView]);
|
||||||
|
|
||||||
useEffect(() => {
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
loadIncidents();
|
loadIncidents();
|
||||||
}, []);
|
}, [])
|
||||||
|
);
|
||||||
|
|
||||||
async function loadIncidents() {
|
async function loadIncidents() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -98,7 +100,7 @@ export default function HomeScreen() {
|
|||||||
backgroundColor:
|
backgroundColor:
|
||||||
item.status === "open"
|
item.status === "open"
|
||||||
? "#f59e0b20"
|
? "#f59e0b20"
|
||||||
: "#22c55e20",
|
: "#88D65620",
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
|
|||||||
+2
-2
@@ -46,7 +46,7 @@ const LABEL_STYLE = {
|
|||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
type VoiceField = "title" | "symptom" | "rootCause" | "fix";
|
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 }>();
|
||||||
@@ -224,7 +224,7 @@ export default function NewIncidentScreen() {
|
|||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Text style={[LABEL_STYLE, { marginBottom: 6 }]}>Service</Text>
|
{labelRow("Service", "service")}
|
||||||
<TextInput
|
<TextInput
|
||||||
style={FIELD_STYLE}
|
style={FIELD_STYLE}
|
||||||
value={form.service}
|
value={form.service}
|
||||||
|
|||||||
+208
-54
@@ -6,22 +6,33 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
Switch,
|
Switch,
|
||||||
Pressable,
|
Pressable,
|
||||||
Alert,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
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";
|
||||||
|
import type { GitProvider } from "@/lib/git";
|
||||||
|
|
||||||
const KEYS = {
|
const KEYS = {
|
||||||
INPUT_MODE: "pref_input_mode",
|
INPUT_MODE: "pref_input_mode",
|
||||||
HOME_VIEW: "pref_home_view",
|
HOME_VIEW: "pref_home_view",
|
||||||
AI_ENABLED: "pref_ai_enabled",
|
AI_ENABLED: "pref_ai_enabled",
|
||||||
AI_KEY: "pref_ai_key",
|
AI_KEY: "pref_ai_key",
|
||||||
|
MD_TEMPLATE: "md_template",
|
||||||
|
GIT_PROVIDER: "git_provider",
|
||||||
|
// Gitea
|
||||||
GITEA_URL: "gitea_url",
|
GITEA_URL: "gitea_url",
|
||||||
GITEA_TOKEN: "gitea_token",
|
GITEA_TOKEN: "gitea_token",
|
||||||
GITEA_OWNER: "gitea_owner",
|
GITEA_OWNER: "gitea_owner",
|
||||||
GITEA_REPO: "gitea_repo",
|
GITEA_REPO: "gitea_repo",
|
||||||
MD_TEMPLATE: "md_template",
|
// GitHub
|
||||||
|
GITHUB_TOKEN: "github_token",
|
||||||
|
GITHUB_OWNER: "github_owner",
|
||||||
|
GITHUB_REPO: "github_repo",
|
||||||
|
// GitLab
|
||||||
|
GITLAB_URL: "gitlab_url",
|
||||||
|
GITLAB_TOKEN: "gitlab_token",
|
||||||
|
GITLAB_OWNER: "gitlab_owner",
|
||||||
|
GITLAB_REPO: "gitlab_repo",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const LABEL = {
|
const LABEL = {
|
||||||
@@ -53,40 +64,76 @@ const INPUT = {
|
|||||||
fontFamily: "monospace",
|
fontFamily: "monospace",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const PROVIDER_LABELS: Record<GitProvider, string> = {
|
||||||
|
gitea: "Gitea",
|
||||||
|
github: "GitHub",
|
||||||
|
gitlab: "GitLab",
|
||||||
|
};
|
||||||
|
|
||||||
export default function SettingsScreen() {
|
export default function SettingsScreen() {
|
||||||
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);
|
||||||
const [aiKey, setAiKey] = useState("");
|
const [aiKey, setAiKey] = useState("");
|
||||||
|
const [mdTemplate, setMdTemplate] = useState(DEFAULT_TEMPLATE);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
|
||||||
|
// Git provider
|
||||||
|
const [gitProvider, setGitProvider] = useState<GitProvider>("gitea");
|
||||||
|
// Gitea
|
||||||
const [giteaUrl, setGiteaUrl] = useState("");
|
const [giteaUrl, setGiteaUrl] = useState("");
|
||||||
const [giteaToken, setGiteaToken] = useState("");
|
const [giteaToken, setGiteaToken] = useState("");
|
||||||
const [giteaOwner, setGiteaOwner] = useState("");
|
const [giteaOwner, setGiteaOwner] = useState("");
|
||||||
const [giteaRepo, setGiteaRepo] = useState("");
|
const [giteaRepo, setGiteaRepo] = useState("");
|
||||||
const [mdTemplate, setMdTemplate] = useState(DEFAULT_TEMPLATE);
|
// GitHub
|
||||||
const [saved, setSaved] = useState(false);
|
const [githubToken, setGithubToken] = useState("");
|
||||||
|
const [githubOwner, setGithubOwner] = useState("");
|
||||||
|
const [githubRepo, setGithubRepo] = useState("");
|
||||||
|
// GitLab
|
||||||
|
const [gitlabUrl, setGitlabUrl] = useState("");
|
||||||
|
const [gitlabToken, setGitlabToken] = useState("");
|
||||||
|
const [gitlabOwner, setGitlabOwner] = useState("");
|
||||||
|
const [gitlabRepo, setGitlabRepo] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const [im, hv, ai, key, gu, gt, go, gr, tpl] = await Promise.all([
|
const [im, hv, ai, key, tpl, gp, gu, gt, go, gr, ghu, ghown, ghrepo, glu, glt, glo, glr] =
|
||||||
|
await Promise.all([
|
||||||
SecureStore.getItemAsync(KEYS.INPUT_MODE),
|
SecureStore.getItemAsync(KEYS.INPUT_MODE),
|
||||||
SecureStore.getItemAsync(KEYS.HOME_VIEW),
|
SecureStore.getItemAsync(KEYS.HOME_VIEW),
|
||||||
SecureStore.getItemAsync(KEYS.AI_ENABLED),
|
SecureStore.getItemAsync(KEYS.AI_ENABLED),
|
||||||
SecureStore.getItemAsync(KEYS.AI_KEY),
|
SecureStore.getItemAsync(KEYS.AI_KEY),
|
||||||
|
SecureStore.getItemAsync(KEYS.MD_TEMPLATE),
|
||||||
|
SecureStore.getItemAsync(KEYS.GIT_PROVIDER),
|
||||||
SecureStore.getItemAsync(KEYS.GITEA_URL),
|
SecureStore.getItemAsync(KEYS.GITEA_URL),
|
||||||
SecureStore.getItemAsync(KEYS.GITEA_TOKEN),
|
SecureStore.getItemAsync(KEYS.GITEA_TOKEN),
|
||||||
SecureStore.getItemAsync(KEYS.GITEA_OWNER),
|
SecureStore.getItemAsync(KEYS.GITEA_OWNER),
|
||||||
SecureStore.getItemAsync(KEYS.GITEA_REPO),
|
SecureStore.getItemAsync(KEYS.GITEA_REPO),
|
||||||
SecureStore.getItemAsync(KEYS.MD_TEMPLATE),
|
SecureStore.getItemAsync(KEYS.GITHUB_TOKEN),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITHUB_OWNER),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITHUB_REPO),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITLAB_URL),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITLAB_TOKEN),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITLAB_OWNER),
|
||||||
|
SecureStore.getItemAsync(KEYS.GITLAB_REPO),
|
||||||
]);
|
]);
|
||||||
if (im) setInputMode(im as "voice" | "form");
|
if (im) setInputMode(im as "voice" | "form");
|
||||||
if (hv) setHomeView(hv as "dashboard" | "capture");
|
if (hv) setHomeView(hv as "dashboard" | "capture");
|
||||||
if (ai) setAiEnabled(ai === "true");
|
if (ai) setAiEnabled(ai === "true");
|
||||||
if (key) setAiKey(key);
|
if (key) setAiKey(key);
|
||||||
|
if (tpl) setMdTemplate(tpl);
|
||||||
|
if (gp) setGitProvider(gp as GitProvider);
|
||||||
if (gu) setGiteaUrl(gu);
|
if (gu) setGiteaUrl(gu);
|
||||||
if (gt) setGiteaToken(gt);
|
if (gt) setGiteaToken(gt);
|
||||||
if (go) setGiteaOwner(go);
|
if (go) setGiteaOwner(go);
|
||||||
if (gr) setGiteaRepo(gr);
|
if (gr) setGiteaRepo(gr);
|
||||||
if (tpl) setMdTemplate(tpl);
|
if (ghu) setGithubToken(ghu);
|
||||||
|
if (ghown) setGithubOwner(ghown);
|
||||||
|
if (ghrepo) setGithubRepo(ghrepo);
|
||||||
|
if (glu) setGitlabUrl(glu);
|
||||||
|
if (glt) setGitlabToken(glt);
|
||||||
|
if (glo) setGitlabOwner(glo);
|
||||||
|
if (glr) setGitlabRepo(glr);
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -96,11 +143,19 @@ export default function SettingsScreen() {
|
|||||||
SecureStore.setItemAsync(KEYS.HOME_VIEW, homeView),
|
SecureStore.setItemAsync(KEYS.HOME_VIEW, homeView),
|
||||||
SecureStore.setItemAsync(KEYS.AI_ENABLED, String(aiEnabled)),
|
SecureStore.setItemAsync(KEYS.AI_ENABLED, String(aiEnabled)),
|
||||||
SecureStore.setItemAsync(KEYS.AI_KEY, aiKey),
|
SecureStore.setItemAsync(KEYS.AI_KEY, aiKey),
|
||||||
|
SecureStore.setItemAsync(KEYS.MD_TEMPLATE, mdTemplate),
|
||||||
|
SecureStore.setItemAsync(KEYS.GIT_PROVIDER, gitProvider),
|
||||||
SecureStore.setItemAsync(KEYS.GITEA_URL, giteaUrl),
|
SecureStore.setItemAsync(KEYS.GITEA_URL, giteaUrl),
|
||||||
SecureStore.setItemAsync(KEYS.GITEA_TOKEN, giteaToken),
|
SecureStore.setItemAsync(KEYS.GITEA_TOKEN, giteaToken),
|
||||||
SecureStore.setItemAsync(KEYS.GITEA_OWNER, giteaOwner),
|
SecureStore.setItemAsync(KEYS.GITEA_OWNER, giteaOwner),
|
||||||
SecureStore.setItemAsync(KEYS.GITEA_REPO, giteaRepo),
|
SecureStore.setItemAsync(KEYS.GITEA_REPO, giteaRepo),
|
||||||
SecureStore.setItemAsync(KEYS.MD_TEMPLATE, mdTemplate),
|
SecureStore.setItemAsync(KEYS.GITHUB_TOKEN, githubToken),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITHUB_OWNER, githubOwner),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITHUB_REPO, githubRepo),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITLAB_URL, gitlabUrl),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITLAB_TOKEN, gitlabToken),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITLAB_OWNER, gitlabOwner),
|
||||||
|
SecureStore.setItemAsync(KEYS.GITLAB_REPO, gitlabRepo),
|
||||||
]);
|
]);
|
||||||
setSaved(true);
|
setSaved(true);
|
||||||
setTimeout(() => setSaved(false), 2000);
|
setTimeout(() => setSaved(false), 2000);
|
||||||
@@ -171,8 +226,8 @@ export default function SettingsScreen() {
|
|||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
color: value === opt.key ? "#fff" : Colors.text2,
|
color: value === opt.key ? Colors.bg : Colors.text2,
|
||||||
fontWeight: "600",
|
fontWeight: "700",
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -185,6 +240,38 @@ export default function SettingsScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Field({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder,
|
||||||
|
secure,
|
||||||
|
url,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
onChange: (v: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
secure?: boolean;
|
||||||
|
url?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Text style={LABEL}>{label}</Text>
|
||||||
|
<TextInput
|
||||||
|
style={INPUT}
|
||||||
|
value={value}
|
||||||
|
onChangeText={onChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
placeholderTextColor={Colors.textDim}
|
||||||
|
secureTextEntry={secure}
|
||||||
|
autoCapitalize="none"
|
||||||
|
keyboardType={url ? "url" : "default"}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={{ flex: 1, backgroundColor: Colors.bg }}
|
style={{ flex: 1, backgroundColor: Colors.bg }}
|
||||||
@@ -220,59 +307,126 @@ export default function SettingsScreen() {
|
|||||||
onValueChange={setAiEnabled}
|
onValueChange={setAiEnabled}
|
||||||
/>
|
/>
|
||||||
{aiEnabled && (
|
{aiEnabled && (
|
||||||
<>
|
<Field
|
||||||
<Text style={LABEL}>API Key</Text>
|
label="API Key"
|
||||||
<TextInput
|
|
||||||
style={INPUT}
|
|
||||||
value={aiKey}
|
value={aiKey}
|
||||||
onChangeText={setAiKey}
|
onChange={setAiKey}
|
||||||
placeholder="sk-..."
|
placeholder="sk-..."
|
||||||
placeholderTextColor={Colors.textDim}
|
secure
|
||||||
secureTextEntry
|
/>
|
||||||
autoCapitalize="none"
|
)}
|
||||||
|
|
||||||
|
<Text style={SECTION_TITLE}>Git Repository</Text>
|
||||||
|
|
||||||
|
<SegmentRow
|
||||||
|
label="Provider"
|
||||||
|
value={gitProvider}
|
||||||
|
onChange={(v) => setGitProvider(v as GitProvider)}
|
||||||
|
options={(["gitea", "github", "gitlab"] as GitProvider[]).map((p) => ({
|
||||||
|
key: p,
|
||||||
|
label: PROVIDER_LABELS[p],
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{gitProvider === "gitea" && (
|
||||||
|
<>
|
||||||
|
<Field
|
||||||
|
label="Instance URL"
|
||||||
|
value={giteaUrl}
|
||||||
|
onChange={setGiteaUrl}
|
||||||
|
placeholder="https://homegit.example.com"
|
||||||
|
url
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Token"
|
||||||
|
value={giteaToken}
|
||||||
|
onChange={setGiteaToken}
|
||||||
|
placeholder="Bearer token"
|
||||||
|
secure
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Owner"
|
||||||
|
value={giteaOwner}
|
||||||
|
onChange={setGiteaOwner}
|
||||||
|
placeholder="username or org"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Repository"
|
||||||
|
value={giteaRepo}
|
||||||
|
onChange={setGiteaRepo}
|
||||||
|
placeholder="incidents"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text style={SECTION_TITLE}>Gitea</Text>
|
{gitProvider === "github" && (
|
||||||
<Text style={LABEL}>Instance URL</Text>
|
<>
|
||||||
<TextInput
|
<View
|
||||||
style={INPUT}
|
style={{
|
||||||
value={giteaUrl}
|
backgroundColor: Colors.surface,
|
||||||
onChangeText={setGiteaUrl}
|
borderRadius: 8,
|
||||||
placeholder="https://homegit.gyozamancave.fr"
|
padding: 12,
|
||||||
placeholderTextColor={Colors.textDim}
|
marginBottom: 14,
|
||||||
autoCapitalize="none"
|
borderLeftWidth: 3,
|
||||||
keyboardType="url"
|
borderLeftColor: Colors.primary,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: Colors.text2, fontSize: 13 }}>
|
||||||
|
Uses api.github.com — no instance URL required.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Field
|
||||||
|
label="Personal Access Token"
|
||||||
|
value={githubToken}
|
||||||
|
onChange={setGithubToken}
|
||||||
|
placeholder="ghp_..."
|
||||||
|
secure
|
||||||
/>
|
/>
|
||||||
<Text style={LABEL}>Token</Text>
|
<Field
|
||||||
<TextInput
|
label="Owner (username or org)"
|
||||||
style={INPUT}
|
value={githubOwner}
|
||||||
value={giteaToken}
|
onChange={setGithubOwner}
|
||||||
onChangeText={setGiteaToken}
|
placeholder="your-username"
|
||||||
placeholder="Bearer token"
|
|
||||||
placeholderTextColor={Colors.textDim}
|
|
||||||
secureTextEntry
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
/>
|
||||||
<Text style={LABEL}>Owner</Text>
|
<Field
|
||||||
<TextInput
|
label="Repository"
|
||||||
style={INPUT}
|
value={githubRepo}
|
||||||
value={giteaOwner}
|
onChange={setGithubRepo}
|
||||||
onChangeText={setGiteaOwner}
|
|
||||||
placeholder="username or org"
|
|
||||||
placeholderTextColor={Colors.textDim}
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
|
||||||
<Text style={LABEL}>Repository</Text>
|
|
||||||
<TextInput
|
|
||||||
style={INPUT}
|
|
||||||
value={giteaRepo}
|
|
||||||
onChangeText={setGiteaRepo}
|
|
||||||
placeholder="incidents"
|
placeholder="incidents"
|
||||||
placeholderTextColor={Colors.textDim}
|
|
||||||
autoCapitalize="none"
|
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{gitProvider === "gitlab" && (
|
||||||
|
<>
|
||||||
|
<Field
|
||||||
|
label="Instance URL"
|
||||||
|
value={gitlabUrl}
|
||||||
|
onChange={setGitlabUrl}
|
||||||
|
placeholder="https://gitlab.com"
|
||||||
|
url
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Personal Access Token"
|
||||||
|
value={gitlabToken}
|
||||||
|
onChange={setGitlabToken}
|
||||||
|
placeholder="glpat-..."
|
||||||
|
secure
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Namespace (user or group)"
|
||||||
|
value={gitlabOwner}
|
||||||
|
onChange={setGitlabOwner}
|
||||||
|
placeholder="your-username"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
label="Repository"
|
||||||
|
value={gitlabRepo}
|
||||||
|
onChange={setGitlabRepo}
|
||||||
|
placeholder="incidents"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<Text style={SECTION_TITLE}>Markdown Template</Text>
|
<Text style={SECTION_TITLE}>Markdown Template</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -294,7 +448,7 @@ export default function SettingsScreen() {
|
|||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ color: "#fff", fontSize: 16, fontWeight: "700" }}>
|
<Text style={{ color: Colors.bg, fontSize: 16, fontWeight: "700" }}>
|
||||||
{saved ? "Saved ✓" : "Save Settings"}
|
{saved ? "Saved ✓" : "Save Settings"}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
+12
-12
@@ -1,16 +1,16 @@
|
|||||||
export const Colors = {
|
export const Colors = {
|
||||||
bg: "#0f1117",
|
bg: "#1B2433",
|
||||||
surface: "#1a1d27",
|
surface: "#222D3D",
|
||||||
surface2: "#252836",
|
surface2: "#293648",
|
||||||
primary: "#6366f1",
|
primary: "#88D656",
|
||||||
primaryDim: "#4338ca",
|
primaryDim: "#5FA33A",
|
||||||
success: "#22c55e",
|
success: "#88D656",
|
||||||
warning: "#f59e0b",
|
warning: "#F5A623",
|
||||||
danger: "#ef4444",
|
danger: "#EF4444",
|
||||||
text1: "#f1f5f9",
|
text1: "#EEF2F5",
|
||||||
text2: "#94a3b8",
|
text2: "#8FA5BF",
|
||||||
textDim: "#64748b",
|
textDim: "#4E6480",
|
||||||
border: "#2d3147",
|
border: "#2E3F55",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const FontFamily = {
|
export const FontFamily = {
|
||||||
|
|||||||
+174
@@ -0,0 +1,174 @@
|
|||||||
|
import { incidentToFilename, renderMarkdown } from "./markdown";
|
||||||
|
import type { Incident } from "@/types/incident";
|
||||||
|
|
||||||
|
export type GitProvider = "gitea" | "github" | "gitlab";
|
||||||
|
|
||||||
|
export interface GitConfig {
|
||||||
|
provider: GitProvider;
|
||||||
|
url?: string; // required for gitea and self-hosted gitlab
|
||||||
|
token: string;
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PushResult {
|
||||||
|
success: boolean;
|
||||||
|
url?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function pushIncidentToGit(
|
||||||
|
incident: Incident,
|
||||||
|
config: GitConfig,
|
||||||
|
markdownTemplate?: string
|
||||||
|
): Promise<PushResult> {
|
||||||
|
switch (config.provider) {
|
||||||
|
case "gitea":
|
||||||
|
return pushGitea(incident, config, markdownTemplate);
|
||||||
|
case "github":
|
||||||
|
return pushGitHub(incident, config, markdownTemplate);
|
||||||
|
case "gitlab":
|
||||||
|
return pushGitLab(incident, config, markdownTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushGitea(
|
||||||
|
incident: Incident,
|
||||||
|
config: GitConfig,
|
||||||
|
markdownTemplate?: string
|
||||||
|
): Promise<PushResult> {
|
||||||
|
const filepath = incidentToFilename(incident);
|
||||||
|
const content = renderMarkdown(incident, markdownTemplate);
|
||||||
|
const base64Content = btoa(unescape(encodeURIComponent(content)));
|
||||||
|
const apiBase = (config.url ?? "").replace(/\/$/, "");
|
||||||
|
const endpoint = `${apiBase}/api/v1/repos/${config.owner}/${config.repo}/contents/${filepath}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = await fetch(endpoint, {
|
||||||
|
headers: { Authorization: `Bearer ${config.token}` },
|
||||||
|
});
|
||||||
|
let sha: string | undefined;
|
||||||
|
if (existing.ok) {
|
||||||
|
const data = await existing.json() as { sha?: string };
|
||||||
|
sha = data.sha;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: Record<string, string> = {
|
||||||
|
message: `incident: ${incident.title}`,
|
||||||
|
content: base64Content,
|
||||||
|
};
|
||||||
|
if (sha) body.sha = sha;
|
||||||
|
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.token}`,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return { success: false, error: `HTTP ${res.status}: ${await res.text()}` };
|
||||||
|
}
|
||||||
|
const data = await res.json() as { content?: { html_url?: string } };
|
||||||
|
return { success: true, url: data.content?.html_url };
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushGitHub(
|
||||||
|
incident: Incident,
|
||||||
|
config: GitConfig,
|
||||||
|
markdownTemplate?: string
|
||||||
|
): Promise<PushResult> {
|
||||||
|
const filepath = incidentToFilename(incident);
|
||||||
|
const content = renderMarkdown(incident, markdownTemplate);
|
||||||
|
const base64Content = btoa(unescape(encodeURIComponent(content)));
|
||||||
|
const endpoint = `https://api.github.com/repos/${config.owner}/${config.repo}/contents/${filepath}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existing = await fetch(endpoint, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.token}`,
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
let sha: string | undefined;
|
||||||
|
if (existing.ok) {
|
||||||
|
const data = await existing.json() as { sha?: string };
|
||||||
|
sha = data.sha;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: Record<string, string> = {
|
||||||
|
message: `incident: ${incident.title}`,
|
||||||
|
content: base64Content,
|
||||||
|
};
|
||||||
|
if (sha) body.sha = sha;
|
||||||
|
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${config.token}`,
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return { success: false, error: `HTTP ${res.status}: ${await res.text()}` };
|
||||||
|
}
|
||||||
|
const data = await res.json() as { content?: { html_url?: string } };
|
||||||
|
return { success: true, url: data.content?.html_url };
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pushGitLab(
|
||||||
|
incident: Incident,
|
||||||
|
config: GitConfig,
|
||||||
|
markdownTemplate?: string
|
||||||
|
): Promise<PushResult> {
|
||||||
|
const filepath = incidentToFilename(incident);
|
||||||
|
const content = renderMarkdown(incident, markdownTemplate);
|
||||||
|
const base64Content = btoa(unescape(encodeURIComponent(content)));
|
||||||
|
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 existing = await fetch(`${endpoint}?ref=HEAD`, {
|
||||||
|
headers: { "PRIVATE-TOKEN": config.token },
|
||||||
|
});
|
||||||
|
const method = existing.ok ? "PUT" : "POST";
|
||||||
|
|
||||||
|
const res = await fetch(endpoint, {
|
||||||
|
method,
|
||||||
|
headers: {
|
||||||
|
"PRIVATE-TOKEN": config.token,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
branch: "main",
|
||||||
|
content: base64Content,
|
||||||
|
encoding: "base64",
|
||||||
|
commit_message: `incident: ${incident.title}`,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
return { success: false, error: `HTTP ${res.status}: ${await res.text()}` };
|
||||||
|
}
|
||||||
|
const data = await res.json() as { file_path?: string };
|
||||||
|
const webUrl = data.file_path
|
||||||
|
? `${apiBase}/${config.owner}/${config.repo}/-/blob/main/${data.file_path}`
|
||||||
|
: undefined;
|
||||||
|
return { success: true, url: webUrl };
|
||||||
|
} catch (e) {
|
||||||
|
return { success: false, error: e instanceof Error ? e.message : String(e) };
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user