Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83fe089ea5 | |||
| 947ae5917c |
@@ -42,15 +42,13 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Set up Android SDK
|
|
||||||
uses: android-actions/setup-android@v3
|
|
||||||
|
|
||||||
- name: Accept Android SDK licenses
|
- name: Accept Android SDK licenses
|
||||||
run: yes | sdkmanager --licenses || true
|
run: yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses || true
|
||||||
|
|
||||||
- name: Install SDK components
|
- name: Install SDK components
|
||||||
run: |
|
run: |
|
||||||
sdkmanager "platform-tools" "platforms;android-35" "build-tools;35.0.0"
|
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" \
|
||||||
|
"platform-tools" "platforms;android-35" "build-tools;35.0.0"
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|||||||
+10
-50
@@ -10,10 +10,9 @@ import {
|
|||||||
import { useLocalSearchParams, router, useNavigation } from "expo-router";
|
import { useLocalSearchParams, router, useNavigation } from "expo-router";
|
||||||
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 } from "@/lib/db";
|
import { getIncidentById, updateIncident, deleteIncident, markIncidentPushed } from "@/lib/db";
|
||||||
import { renderMarkdown } from "@/lib/markdown";
|
import { renderMarkdown } from "@/lib/markdown";
|
||||||
import { pushIncidentToGit } from "@/lib/git";
|
import { pushIncidentToGit, loadGitConfig } 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";
|
||||||
|
|
||||||
@@ -81,60 +80,21 @@ export default function IncidentDetailScreen() {
|
|||||||
|
|
||||||
async function handleGitPush() {
|
async function handleGitPush() {
|
||||||
if (!incident) return;
|
if (!incident) return;
|
||||||
const [provider, template, giteaUrl, giteaToken, giteaOwner, giteaRepo, githubToken, githubOwner, githubRepo, gitlabUrl, gitlabToken, gitlabOwner, gitlabRepo] =
|
const [config, template] = await Promise.all([
|
||||||
await Promise.all([
|
loadGitConfig(),
|
||||||
SecureStore.getItemAsync("git_provider"),
|
|
||||||
SecureStore.getItemAsync("md_template"),
|
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"),
|
|
||||||
]);
|
]);
|
||||||
|
if (!config) {
|
||||||
const p = (provider ?? "gitea") as GitProvider;
|
Alert.alert("Git not configured", "Set up a git provider in Settings first.");
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const needsUrl = p === "gitea" || p === "gitlab";
|
|
||||||
if (!token || !owner || !repo || (needsUrl && !url)) {
|
|
||||||
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 pushIncidentToGit(
|
const result = await pushIncidentToGit(incident, config, template ?? undefined);
|
||||||
incident,
|
|
||||||
{ provider: p, url, token, owner, repo },
|
|
||||||
template ?? undefined
|
|
||||||
);
|
|
||||||
setPushing(false);
|
setPushing(false);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
await markIncidentPushed(incident.id);
|
||||||
|
setIncident((prev) => prev ? { ...prev, gitPushedAt: now } : prev);
|
||||||
Alert.alert("Pushed", 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");
|
||||||
|
|||||||
+255
-20
@@ -5,16 +5,32 @@ import {
|
|||||||
FlatList,
|
FlatList,
|
||||||
Pressable,
|
Pressable,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
|
Alert,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { router, useFocusEffect } from "expo-router";
|
import { router, useFocusEffect } from "expo-router";
|
||||||
import { getIncidents } from "@/lib/db";
|
import * as SecureStore from "expo-secure-store";
|
||||||
|
import { getIncidents, markIncidentPushed } from "@/lib/db";
|
||||||
|
import { pushIncidentToGit, loadGitConfig } from "@/lib/git";
|
||||||
|
import type { GitConfig } from "@/lib/git";
|
||||||
import { useSettings } from "@/hooks/useSettings";
|
import { useSettings } from "@/hooks/useSettings";
|
||||||
import type { Incident } from "@/types/incident";
|
import type { Incident } from "@/types/incident";
|
||||||
import { Colors } from "@/constants/theme";
|
import { Colors } from "@/constants/theme";
|
||||||
|
|
||||||
|
function gitDotColor(inc: Incident): string {
|
||||||
|
if (!inc.gitPushedAt) return Colors.danger;
|
||||||
|
if (inc.gitPushedAt < inc.updatedAt) return Colors.warning;
|
||||||
|
return Colors.success;
|
||||||
|
}
|
||||||
|
|
||||||
export default function HomeScreen() {
|
export default function HomeScreen() {
|
||||||
const [incidents, setIncidents] = useState<Incident[]>([]);
|
const [incidents, setIncidents] = useState<Incident[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [gitConfig, setGitConfig] = useState<GitConfig | null>(null);
|
||||||
|
const [mdTemplate, setMdTemplate] = useState<string | undefined>(undefined);
|
||||||
|
const [selectionMode, setSelectionMode] = useState(false);
|
||||||
|
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||||
|
const [bulkPushing, setBulkPushing] = useState(false);
|
||||||
|
const [bulkProgress, setBulkProgress] = useState({ done: 0, total: 0 });
|
||||||
const { settings, ready } = useSettings();
|
const { settings, ready } = useSettings();
|
||||||
const didRedirect = useRef(false);
|
const didRedirect = useRef(false);
|
||||||
|
|
||||||
@@ -28,24 +44,99 @@ export default function HomeScreen() {
|
|||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
loadIncidents();
|
load();
|
||||||
}, [])
|
}, [])
|
||||||
);
|
);
|
||||||
|
|
||||||
async function loadIncidents() {
|
async function load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await getIncidents(10);
|
const [data, config, tpl] = await Promise.all([
|
||||||
|
getIncidents(100),
|
||||||
|
loadGitConfig(),
|
||||||
|
SecureStore.getItemAsync("md_template"),
|
||||||
|
]);
|
||||||
setIncidents(data);
|
setIncidents(data);
|
||||||
|
setGitConfig(config);
|
||||||
|
setMdTemplate(tpl ?? undefined);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enterSelectionMode(id: string) {
|
||||||
|
setSelectionMode(true);
|
||||||
|
setSelected(new Set([id]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSelection(id: string) {
|
||||||
|
setSelected((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(id)) next.delete(id);
|
||||||
|
else next.add(id);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitSelectionMode() {
|
||||||
|
setSelectionMode(false);
|
||||||
|
setSelected(new Set());
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllUnpushed() {
|
||||||
|
const ids = incidents
|
||||||
|
.filter((i) => !i.gitPushedAt || i.gitPushedAt < i.updatedAt)
|
||||||
|
.map((i) => i.id);
|
||||||
|
setSelected(new Set(ids));
|
||||||
|
}
|
||||||
|
|
||||||
|
const unpushedCount = incidents.filter(
|
||||||
|
(i) => !i.gitPushedAt || i.gitPushedAt < i.updatedAt
|
||||||
|
).length;
|
||||||
|
|
||||||
|
async function handleBulkPush() {
|
||||||
|
if (!gitConfig) {
|
||||||
|
Alert.alert("Git not configured", "Set up a git provider in Settings first.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const toPush = incidents.filter((i) => selected.has(i.id));
|
||||||
|
if (toPush.length === 0) return;
|
||||||
|
|
||||||
|
setBulkPushing(true);
|
||||||
|
setBulkProgress({ done: 0, total: toPush.length });
|
||||||
|
|
||||||
|
let ok = 0;
|
||||||
|
let fail = 0;
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
for (let i = 0; i < toPush.length; i++) {
|
||||||
|
const inc = toPush[i];
|
||||||
|
const result = await pushIncidentToGit(inc, gitConfig, mdTemplate);
|
||||||
|
if (result.success) {
|
||||||
|
await markIncidentPushed(inc.id);
|
||||||
|
setIncidents((prev) =>
|
||||||
|
prev.map((x) => (x.id === inc.id ? { ...x, gitPushedAt: now } : x))
|
||||||
|
);
|
||||||
|
ok++;
|
||||||
|
} else {
|
||||||
|
fail++;
|
||||||
|
}
|
||||||
|
setBulkProgress({ done: i + 1, total: toPush.length });
|
||||||
|
}
|
||||||
|
|
||||||
|
setBulkPushing(false);
|
||||||
|
exitSelectionMode();
|
||||||
|
|
||||||
|
if (fail === 0) {
|
||||||
|
Alert.alert("Done", `${ok} incident${ok > 1 ? "s" : ""} pushed.`);
|
||||||
|
} else {
|
||||||
|
Alert.alert("Partial", `${ok} pushed, ${fail} failed.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const gitConfigured = gitConfig !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: Colors.bg }}>
|
<View style={{ flex: 1, backgroundColor: Colors.bg }}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<ActivityIndicator
|
<ActivityIndicator color={Colors.primary} style={{ marginTop: 48 }} />
|
||||||
color={Colors.primary}
|
|
||||||
style={{ marginTop: 48 }}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={incidents}
|
data={incidents}
|
||||||
@@ -63,19 +154,29 @@ export default function HomeScreen() {
|
|||||||
No incidents. Press + to log one.
|
No incidents. Press + to log one.
|
||||||
</Text>
|
</Text>
|
||||||
}
|
}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => {
|
||||||
|
const isSelected = selected.has(item.id);
|
||||||
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => router.push(`/incident/${item.id}`)}
|
onPress={() =>
|
||||||
|
selectionMode
|
||||||
|
? toggleSelection(item.id)
|
||||||
|
: router.push(`/incident/${item.id}`)
|
||||||
|
}
|
||||||
|
onLongPress={() => !selectionMode && enterSelectionMode(item.id)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: Colors.surface,
|
backgroundColor: isSelected ? Colors.surface2 : Colors.surface,
|
||||||
borderRadius: 8,
|
borderRadius: 8,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
borderLeftWidth: 3,
|
borderLeftWidth: 3,
|
||||||
borderLeftColor:
|
borderLeftColor:
|
||||||
item.status === "open" ? Colors.warning : Colors.success,
|
item.status === "open" ? Colors.warning : Colors.success,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -95,16 +196,24 @@ export default function HomeScreen() {
|
|||||||
>
|
>
|
||||||
{item.title || "Untitled"}
|
{item.title || "Untitled"}
|
||||||
</Text>
|
</Text>
|
||||||
|
<View style={{ flexDirection: "row", alignItems: "center", marginLeft: 8, gap: 6 }}>
|
||||||
|
{gitConfigured && !selectionMode && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: gitDotColor(item),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
item.status === "open"
|
item.status === "open" ? "#f59e0b20" : "#88D65620",
|
||||||
? "#f59e0b20"
|
|
||||||
: "#88D65620",
|
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
paddingHorizontal: 8,
|
paddingHorizontal: 8,
|
||||||
paddingVertical: 2,
|
paddingVertical: 2,
|
||||||
marginLeft: 8,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
@@ -122,18 +231,46 @@ export default function HomeScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
<Text
|
<Text
|
||||||
style={{ color: Colors.text2, fontSize: 12 }}
|
style={{ color: Colors.text2, fontSize: 12 }}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{item.service || "—"} · {item.createdAt.slice(0, 10)}
|
{item.service || "—"} · {item.createdAt.slice(0, 10)}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</View>
|
||||||
|
|
||||||
|
{selectionMode && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
borderRadius: 11,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: isSelected ? Colors.primary : Colors.border,
|
||||||
|
backgroundColor: isSelected ? Colors.primary : "transparent",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginLeft: 14,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSelected && (
|
||||||
|
<Text
|
||||||
|
style={{ color: Colors.bg, fontSize: 12, fontWeight: "700" }}
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* FAB */}
|
{/* FAB — hidden in selection mode */}
|
||||||
|
{!selectionMode && (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => router.push("/new")}
|
onPress={() => router.push("/new")}
|
||||||
style={{
|
style={{
|
||||||
@@ -149,12 +286,110 @@ export default function HomeScreen() {
|
|||||||
elevation: 6,
|
elevation: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text style={{ color: "#fff", fontSize: 28, lineHeight: 32 }}>+</Text>
|
||||||
style={{ color: "#fff", fontSize: 28, lineHeight: 32 }}
|
</Pressable>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Selection mode action bar */}
|
||||||
|
{selectionMode && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
backgroundColor: Colors.surface,
|
||||||
|
borderTopWidth: 1,
|
||||||
|
borderTopColor: Colors.border,
|
||||||
|
padding: 12,
|
||||||
|
gap: 10,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
+
|
{/* Quick-select shortcuts */}
|
||||||
|
<View style={{ flexDirection: "row", gap: 8 }}>
|
||||||
|
{gitConfigured && unpushedCount > 0 && (
|
||||||
|
<Pressable
|
||||||
|
onPress={selectAllUnpushed}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 6,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Colors.border,
|
||||||
|
padding: 8,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: Colors.text2, fontSize: 12, fontWeight: "600" }}>
|
||||||
|
Select unpushed ({unpushedCount})
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setSelected(new Set(incidents.map((i) => i.id)))}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 6,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Colors.border,
|
||||||
|
padding: 8,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: Colors.text2, fontSize: 12, fontWeight: "600" }}>
|
||||||
|
Select all ({incidents.length})
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Main actions */}
|
||||||
|
<View style={{ flexDirection: "row", gap: 10 }}>
|
||||||
|
<Pressable
|
||||||
|
onPress={exitSelectionMode}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
borderRadius: 8,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: Colors.border,
|
||||||
|
padding: 14,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: Colors.text2, fontSize: 15, fontWeight: "600" }}>
|
||||||
|
Cancel
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
<Pressable
|
||||||
|
onPress={handleBulkPush}
|
||||||
|
disabled={selected.size === 0 || bulkPushing || !gitConfigured}
|
||||||
|
style={{
|
||||||
|
flex: 2,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor:
|
||||||
|
selected.size === 0 || !gitConfigured
|
||||||
|
? Colors.surface2
|
||||||
|
: Colors.primary,
|
||||||
|
padding: 14,
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color:
|
||||||
|
selected.size === 0 || !gitConfigured
|
||||||
|
? Colors.textDim
|
||||||
|
: Colors.bg,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: "700",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{bulkPushing
|
||||||
|
? `Pushing ${bulkProgress.done}/${bulkProgress.total}…`
|
||||||
|
: `Push ${selected.size > 0 ? selected.size : ""} selected`}
|
||||||
|
</Text>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,13 @@ async function migrate(db: SQLite.SQLiteDatabase): Promise<void> {
|
|||||||
INSERT INTO schema_version (version) VALUES (1);
|
INSERT INTO schema_version (version) VALUES (1);
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (current < 2) {
|
||||||
|
await db.execAsync(`
|
||||||
|
ALTER TABLE incidents ADD COLUMN git_pushed_at TEXT;
|
||||||
|
INSERT INTO schema_version (version) VALUES (2);
|
||||||
|
`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- row mapper ---
|
// --- row mapper ---
|
||||||
@@ -60,6 +67,7 @@ interface IncidentRow {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
tags: string;
|
tags: string;
|
||||||
|
git_pushed_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rowToIncident(row: IncidentRow): Incident {
|
function rowToIncident(row: IncidentRow): Incident {
|
||||||
@@ -74,6 +82,7 @@ function rowToIncident(row: IncidentRow): Incident {
|
|||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at,
|
updatedAt: row.updated_at,
|
||||||
tags: JSON.parse(row.tags) as string[],
|
tags: JSON.parse(row.tags) as string[],
|
||||||
|
gitPushedAt: row.git_pushed_at ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +116,7 @@ export async function createIncident(draft: IncidentDraft): Promise<Incident> {
|
|||||||
id,
|
id,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
|
gitPushedAt: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,6 +173,14 @@ export async function deleteIncident(id: string): Promise<void> {
|
|||||||
await db.runAsync("DELETE FROM incidents WHERE id = ?", [id]);
|
await db.runAsync("DELETE FROM incidents WHERE id = ?", [id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function markIncidentPushed(id: string): Promise<void> {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.runAsync(
|
||||||
|
"UPDATE incidents SET git_pushed_at = ? WHERE id = ?",
|
||||||
|
[new Date().toISOString(), id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getMaxTitleNumber(prefix: string): Promise<number> {
|
export async function getMaxTitleNumber(prefix: string): Promise<number> {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
const rows = await db.getAllAsync<{ title: string }>(
|
const rows = await db.getAllAsync<{ title: string }>(
|
||||||
|
|||||||
+39
@@ -1,3 +1,4 @@
|
|||||||
|
import * as SecureStore from "expo-secure-store";
|
||||||
import { incidentToFilename, renderMarkdown } from "./markdown";
|
import { incidentToFilename, renderMarkdown } from "./markdown";
|
||||||
import type { Incident } from "@/types/incident";
|
import type { Incident } from "@/types/incident";
|
||||||
|
|
||||||
@@ -19,6 +20,44 @@ export interface PushResult {
|
|||||||
|
|
||||||
const TIMEOUT_MS = 15_000;
|
const TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
|
export async function loadGitConfig(): Promise<GitConfig | null> {
|
||||||
|
const [provider, gu, gt, go, gr, ghu, ghown, ghrepo, glu, glt, glo, glr] =
|
||||||
|
await Promise.all([
|
||||||
|
SecureStore.getItemAsync("git_provider"),
|
||||||
|
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 = gu ?? undefined;
|
||||||
|
token = gt; owner = go; repo = gr;
|
||||||
|
} else if (p === "github") {
|
||||||
|
token = ghu; owner = ghown; repo = ghrepo;
|
||||||
|
} else {
|
||||||
|
url = glu ?? undefined;
|
||||||
|
token = glt; owner = glo; repo = glr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsUrl = p === "gitea" || p === "gitlab";
|
||||||
|
if (!token || !owner || !repo || (needsUrl && !url)) return null;
|
||||||
|
return { provider: p, url, token, owner, repo };
|
||||||
|
}
|
||||||
|
|
||||||
function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
|
function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||||
|
|||||||
+2
-1
@@ -11,6 +11,7 @@ export interface Incident {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
gitPushedAt: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IncidentDraft = Omit<Incident, "id" | "createdAt" | "updatedAt">;
|
export type IncidentDraft = Omit<Incident, "id" | "createdAt" | "updatedAt" | "gitPushedAt">;
|
||||||
|
|||||||
Reference in New Issue
Block a user