Skip to content

Commit a8c2a61

Browse files
authored
Merge pull request #36 from kc3hack/feat/nenrin/#35-add-decomp-task-widget-for-dashboard
feat: #35 ウィジェット追加など最終調整
2 parents 71c9546 + ba5db82 commit a8c2a61

17 files changed

Lines changed: 1842 additions & 308 deletions

File tree

.DS_Store

6 KB
Binary file not shown.

.github/.DS_Store

6 KB
Binary file not shown.

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
# プロダクト名
2-
<!-- プロダクト名に変更してください -->
1+
# ネボガード(NeboGuard)
32

4-
![プロダクト名](https://kc3.me/cms/wp-content/uploads/2026/02/444e7120d5cdd74aa75f7a94bf8821a5-scaled.png)
3+
![ネボガード(NeboGuard)](https://kc3.me/cms/wp-content/uploads/2026/02/444e7120d5cdd74aa75f7a94bf8821a5-scaled.png)
54
<!-- プロダクト名・イメージ画像を差し変えてください -->
65

76

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
create table if not exists "morning_routine_settings" (
2+
"user_id" text primary key,
3+
"routine_json" text not null,
4+
"created_at" text not null,
5+
"updated_at" text not null
6+
);

backend/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getAllowedOrigins, isAllowedOrigin } from "./lib/origins";
44
import { registerAuthRoutes } from "./routes/auth-routes";
55
import { registerBriefingRoutes } from "./routes/briefing-routes";
66
import { registerCalendarRoutes } from "./routes/calendar-routes";
7+
import { registerMorningRoutineRoutes } from "./routes/morning-routine-routes";
78
import { registerRootRoutes } from "./routes/root-routes";
89
import { registerTaskRoutes } from "./routes/task-routes";
910
import { registerTransitRoutes } from "./routes/transit-routes";
@@ -53,6 +54,7 @@ export function createApp(): App {
5354
registerRootRoutes(app);
5455
registerAuthRoutes(app);
5556
registerBriefingRoutes(app);
57+
registerMorningRoutineRoutes(app);
5658
registerCalendarRoutes(app);
5759
registerTaskRoutes(app);
5860
registerTransitRoutes(app);
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
export type MorningRoutineItem = {
2+
id: string;
3+
label: string;
4+
minutes: number;
5+
};
6+
7+
type RoutineRow = {
8+
routine_json: string;
9+
};
10+
11+
const DEFAULT_MORNING_ROUTINE: MorningRoutineItem[] = [
12+
{ id: "prepare", label: "身支度", minutes: 20 },
13+
{ id: "breakfast", label: "朝食", minutes: 15 },
14+
];
15+
16+
function createRoutineItemId(): string {
17+
if (
18+
typeof crypto !== "undefined" &&
19+
typeof crypto.randomUUID === "function"
20+
) {
21+
return crypto.randomUUID();
22+
}
23+
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
24+
}
25+
26+
function clampMinutes(value: number): number {
27+
if (!Number.isFinite(value)) {
28+
return 0;
29+
}
30+
return Math.min(180, Math.max(0, Math.trunc(value)));
31+
}
32+
33+
function cloneItems(items: MorningRoutineItem[]): MorningRoutineItem[] {
34+
return items.map((item) => ({ ...item }));
35+
}
36+
37+
function normalizeRoutineItems(value: unknown): MorningRoutineItem[] | null {
38+
if (!Array.isArray(value)) {
39+
return null;
40+
}
41+
42+
if (value.length === 0 || value.length > 20) {
43+
return null;
44+
}
45+
46+
const normalized = value
47+
.map((item) => {
48+
if (!item || typeof item !== "object") {
49+
return null;
50+
}
51+
52+
const candidate = item as {
53+
id?: unknown;
54+
label?: unknown;
55+
minutes?: unknown;
56+
};
57+
58+
const label =
59+
typeof candidate.label === "string" ? candidate.label.trim() : "";
60+
if (label.length === 0 || label.length > 40) {
61+
return null;
62+
}
63+
64+
const minutesRaw =
65+
typeof candidate.minutes === "number"
66+
? candidate.minutes
67+
: typeof candidate.minutes === "string"
68+
? Number(candidate.minutes)
69+
: Number.NaN;
70+
const minutes = Number.isFinite(minutesRaw)
71+
? clampMinutes(minutesRaw)
72+
: 0;
73+
74+
const id =
75+
typeof candidate.id === "string" && candidate.id.trim().length > 0
76+
? candidate.id.trim()
77+
: createRoutineItemId();
78+
79+
return { id, label, minutes } satisfies MorningRoutineItem;
80+
})
81+
.filter((item): item is MorningRoutineItem => item !== null);
82+
83+
if (normalized.length === 0) {
84+
return null;
85+
}
86+
87+
return normalized;
88+
}
89+
90+
function parseRoutineJson(json: string): MorningRoutineItem[] | null {
91+
try {
92+
return normalizeRoutineItems(JSON.parse(json));
93+
} catch {
94+
return null;
95+
}
96+
}
97+
98+
export function validateMorningRoutineItems(
99+
value: unknown,
100+
): MorningRoutineItem[] | null {
101+
return normalizeRoutineItems(value);
102+
}
103+
104+
export async function getMorningRoutine(
105+
db: D1Database,
106+
userId: string,
107+
): Promise<MorningRoutineItem[]> {
108+
const row = await db
109+
.prepare(
110+
`
111+
select routine_json
112+
from morning_routine_settings
113+
where user_id = ?
114+
limit 1
115+
`,
116+
)
117+
.bind(userId)
118+
.first<RoutineRow>();
119+
120+
if (!row?.routine_json) {
121+
return cloneItems(DEFAULT_MORNING_ROUTINE);
122+
}
123+
124+
const parsed = parseRoutineJson(row.routine_json);
125+
if (!parsed) {
126+
return cloneItems(DEFAULT_MORNING_ROUTINE);
127+
}
128+
129+
return parsed;
130+
}
131+
132+
export async function saveMorningRoutine(
133+
db: D1Database,
134+
userId: string,
135+
items: MorningRoutineItem[],
136+
): Promise<MorningRoutineItem[]> {
137+
const normalized = normalizeRoutineItems(items);
138+
if (!normalized) {
139+
throw new Error("Invalid morning routine items.");
140+
}
141+
142+
const now = new Date().toISOString();
143+
await db
144+
.prepare(
145+
`
146+
insert into morning_routine_settings (
147+
user_id,
148+
routine_json,
149+
created_at,
150+
updated_at
151+
)
152+
values (?, ?, ?, ?)
153+
on conflict(user_id) do update set
154+
routine_json = excluded.routine_json,
155+
updated_at = excluded.updated_at
156+
`,
157+
)
158+
.bind(userId, JSON.stringify(normalized), now, now)
159+
.run();
160+
161+
return normalized;
162+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import {
2+
getMorningRoutine,
3+
saveMorningRoutine,
4+
validateMorningRoutineItems,
5+
} from "../features/morning-routine/morning-routine.service";
6+
import { getAuthSession } from "../lib/session";
7+
import type { App } from "../types/app";
8+
9+
export function registerMorningRoutineRoutes(app: App): void {
10+
app.get("/briefing/routine", async (c) => {
11+
const authSession = await getAuthSession(c);
12+
if (!authSession) {
13+
return c.json({ error: "Authentication required." }, 401);
14+
}
15+
16+
const items = await getMorningRoutine(c.env.AUTH_DB, authSession.user.id);
17+
return c.json({ items });
18+
});
19+
20+
app.put("/briefing/routine", async (c) => {
21+
const authSession = await getAuthSession(c);
22+
if (!authSession) {
23+
return c.json({ error: "Authentication required." }, 401);
24+
}
25+
26+
const body = await c.req.json().catch(() => null);
27+
const items = validateMorningRoutineItems(body?.items);
28+
if (!items) {
29+
return c.json(
30+
{
31+
error:
32+
"Request body must include non-empty `items` with { id?, label, minutes }.",
33+
},
34+
400,
35+
);
36+
}
37+
38+
const saved = await saveMorningRoutine(
39+
c.env.AUTH_DB,
40+
authSession.user.id,
41+
items,
42+
);
43+
return c.json({ items: saved });
44+
});
45+
}

backend/src/routes/root-routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export function registerRootRoutes(app: App): void {
77
endpoints: [
88
"ALL /api/auth/*",
99
"POST /briefing/morning",
10+
"GET /briefing/routine",
11+
"PUT /briefing/routine",
1012
"GET /calendar/today",
1113
"POST /transit/directions",
1214
"POST /tasks/decompose",

d76869c4364b9cc4.PNG

20 KB
Loading

0 commit comments

Comments
 (0)