3 Commits

Author SHA1 Message Date
billisdead 83fe089ea5 ci: remove android-actions/setup-android, use preinstalled SDK
Release APK / build (push) Has been cancelled
The action consistently fails trying to re-download cmdline-tools
from Google CDN (corrupt zip). The ubuntu-latest runner already ships
a working Android SDK at $ANDROID_HOME — call sdkmanager directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 18:18:50 +02:00
billisdead 947ae5917c feat: git push status indicator + multi-select bulk push
Release APK / build (push) Has been cancelled
- DB migration v2: git_pushed_at column on incidents
- markIncidentPushed(id): stamps the push timestamp
- loadGitConfig(): shared helper in lib/git.ts, removes duplication
- Home screen: colored dot per incident (red=never pushed,
  orange=dirty/modified after push, green=up to date);
  dot visible only when git is configured
- Home screen: long-press enters selection mode, tap toggles;
  action bar with Select unpushed / Select all shortcuts +
  bulk push with sequential progress counter
- [id].tsx: marks incident pushed after successful push,
  updates local state without reload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 18:11:23 +02:00
billisdead 1437e99cbb fix: title increment based on DB max, not stored template only
Release APK / build (push) Has been cancelled
After each incident save, query existing titles with the same prefix
to find the real max number, then use max(dbMax, tplNum) + 1.
Prevents duplicate counters when incidents were created before the
template was configured.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 15:26:10 +02:00
7 changed files with 418 additions and 152 deletions
+3 -5
View File
@@ -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
+12 -52
View File
@@ -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"), if (!config) {
SecureStore.getItemAsync("gitea_token"), Alert.alert("Git not configured", "Set up a git provider in Settings first.");
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;
}
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");
+321 -86
View File
@@ -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,98 +154,242 @@ export default function HomeScreen() {
No incidents. Press + to log one. No incidents. Press + to log one.
</Text> </Text>
} }
renderItem={({ item }) => ( renderItem={({ item }) => {
<Pressable const isSelected = selected.has(item.id);
onPress={() => router.push(`/incident/${item.id}`)} return (
style={{ <Pressable
backgroundColor: Colors.surface, onPress={() =>
borderRadius: 8, selectionMode
padding: 16, ? toggleSelection(item.id)
marginBottom: 10, : router.push(`/incident/${item.id}`)
borderLeftWidth: 3, }
borderLeftColor: onLongPress={() => !selectionMode && enterSelectionMode(item.id)}
item.status === "open" ? Colors.warning : Colors.success,
}}
>
<View
style={{ style={{
backgroundColor: isSelected ? Colors.surface2 : Colors.surface,
borderRadius: 8,
padding: 16,
marginBottom: 10,
borderLeftWidth: 3,
borderLeftColor:
item.status === "open" ? Colors.warning : Colors.success,
flexDirection: "row", flexDirection: "row",
justifyContent: "space-between",
alignItems: "center", alignItems: "center",
marginBottom: 4,
}} }}
> >
<Text <View style={{ flex: 1 }}>
style={{ <View
color: Colors.text1,
fontSize: 15,
fontWeight: "600",
flex: 1,
}}
numberOfLines={1}
>
{item.title || "Untitled"}
</Text>
<View
style={{
backgroundColor:
item.status === "open"
? "#f59e0b20"
: "#88D65620",
borderRadius: 4,
paddingHorizontal: 8,
paddingVertical: 2,
marginLeft: 8,
}}
>
<Text
style={{ style={{
color: flexDirection: "row",
item.status === "open" justifyContent: "space-between",
? Colors.warning alignItems: "center",
: Colors.success, marginBottom: 4,
fontSize: 11,
fontWeight: "700",
textTransform: "uppercase",
}} }}
> >
{item.status} <Text
style={{
color: Colors.text1,
fontSize: 15,
fontWeight: "600",
flex: 1,
}}
numberOfLines={1}
>
{item.title || "Untitled"}
</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
style={{
backgroundColor:
item.status === "open" ? "#f59e0b20" : "#88D65620",
borderRadius: 4,
paddingHorizontal: 8,
paddingVertical: 2,
}}
>
<Text
style={{
color:
item.status === "open"
? Colors.warning
: Colors.success,
fontSize: 11,
fontWeight: "700",
textTransform: "uppercase",
}}
>
{item.status}
</Text>
</View>
</View>
</View>
<Text
style={{ color: Colors.text2, fontSize: 12 }}
numberOfLines={1}
>
{item.service || "—"} · {item.createdAt.slice(0, 10)}
</Text> </Text>
</View> </View>
</View>
<Text {selectionMode && (
style={{ color: Colors.text2, fontSize: 12 }} <View
numberOfLines={1} style={{
> width: 22,
{item.service || "—"} · {item.createdAt.slice(0, 10)} height: 22,
</Text> borderRadius: 11,
</Pressable> 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 */}
<Pressable {!selectionMode && (
onPress={() => router.push("/new")} <Pressable
style={{ onPress={() => router.push("/new")}
position: "absolute", style={{
bottom: 24, position: "absolute",
right: 24, bottom: 24,
width: 56, right: 24,
height: 56, width: 56,
borderRadius: 28, height: 56,
backgroundColor: Colors.primary, borderRadius: 28,
alignItems: "center", backgroundColor: Colors.primary,
justifyContent: "center", alignItems: "center",
elevation: 6, justifyContent: "center",
}} elevation: 6,
> }}
<Text
style={{ color: "#fff", fontSize: 28, lineHeight: 32 }}
> >
+ <Text style={{ color: "#fff", fontSize: 28, lineHeight: 32 }}>+</Text>
</Text> </Pressable>
</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>
</Pressable>
</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> </View>
); );
} }
+9 -8
View File
@@ -16,6 +16,7 @@ import {
updateIncident, updateIncident,
getIncidentById, getIncidentById,
getDistinctServices, getDistinctServices,
getMaxTitleNumber,
} from "@/lib/db"; } from "@/lib/db";
import { Colors } from "@/constants/theme"; import { Colors } from "@/constants/theme";
import { useVoice } from "@/hooks/useVoice"; import { useVoice } from "@/hooks/useVoice";
@@ -50,13 +51,6 @@ const LABEL_STYLE = {
type VoiceField = "title" | "service" | "symptom" | "rootCause" | "fix"; type VoiceField = "title" | "service" | "symptom" | "rootCause" | "fix";
function incrementTemplate(template: string): string {
const match = template.match(/^([\s\S]*?)(\d+)$/);
if (!match) return template;
const [, prefix, numStr] = match;
const next = (parseInt(numStr, 10) + 1).toString().padStart(numStr.length, "0");
return prefix + next;
}
export default function NewIncidentScreen() { export default function NewIncidentScreen() {
const { editId } = useLocalSearchParams<{ editId?: string }>(); const { editId } = useLocalSearchParams<{ editId?: string }>();
@@ -180,7 +174,14 @@ export default function NewIncidentScreen() {
await createIncident(form); await createIncident(form);
const tpl = await SecureStore.getItemAsync("pref_title_template"); const tpl = await SecureStore.getItemAsync("pref_title_template");
if (tpl) { if (tpl) {
await SecureStore.setItemAsync("pref_title_template", incrementTemplate(tpl)); const match = tpl.match(/^([\s\S]*?)(\d+)$/);
if (match) {
const [, prefix, numStr] = match;
const dbMax = await getMaxTitleNumber(prefix);
const tplNum = parseInt(numStr, 10);
const next = (Math.max(dbMax, tplNum) + 1).toString().padStart(numStr.length, "0");
await SecureStore.setItemAsync("pref_title_template", prefix + next);
}
} }
} }
router.back(); router.back();
+32
View File
@@ -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,28 @@ 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> {
const db = await getDb();
const rows = await db.getAllAsync<{ title: string }>(
"SELECT title FROM incidents WHERE title LIKE ?",
[`${prefix}%`]
);
let max = 0;
for (const row of rows) {
const n = parseInt(row.title.slice(prefix.length), 10);
if (!isNaN(n) && n > max) max = n;
}
return max;
}
export async function getDistinctServices(): Promise<string[]> { export async function getDistinctServices(): Promise<string[]> {
const db = await getDb(); const db = await getDb();
const rows = await db.getAllAsync<{ service: string }>( const rows = await db.getAllAsync<{ service: string }>(
+39
View File
@@ -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
View File
@@ -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">;