Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ lab/output/

# opensrc - source code for packages
opensrc/
AGENTS.md
10 changes: 9 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,14 @@ Types are generated to `app/services/type.ts` from the database.

**CamelCasePlugin と `sql` テンプレート**: `sql` テンプレートリテラル内の識別子は CamelCasePlugin で変換されない。`sql` 内でカラムを参照するときは `sql.ref('tableName.columnName')` を使うこと。

### 日時・タイムゾーンの原則

- **DB 保存形式**: ISO 8601(`2026-03-16T02:56:35Z`)。Z付きで保存し、ローカルタイム形式(`2026-03-16 02:56:35`)は使わない
- **DB から読んだ日時のパース**: 必ず `dayjs.utc(value)` を使う。`dayjs(value)` はローカルタイムとして解釈されるため、タイムゾーン変換が正しく動かない
- **表示層でのタイムゾーン変換**: `dayjs.utc(value).tz(timezone)` のパターンを使う
- **batch での書き込み**: GitHub API から取得した ISO 8601 文字列をそのまま DB に保存する。独自のフォーマット変換をかけない
- **`timeFormatTz`**(`batch/helper/timeformat.ts`): レポートやスプレッドシート出力用。内部で `dayjs.utc()` を使用済み

### Path Aliases

Use `~/` prefix for imports from `app/` directory:
Expand Down Expand Up @@ -218,7 +226,7 @@ Org-scoped tables (have `organizationId` column): `companyGithubUsers`, `exportS

### Source Code Reference

Source code for dependencies is available in `opensrc/` for deeper understanding of implementation details. See `opensrc/sources.json` for the list of available packages.
Source code for dependencies is available in `opensrc/` for deeper understanding of implementation details. See `opensrc/sources.json` for the list of available packages and their versions. Use this source code when you need to understand how a package works internally, not just its types/interface.

```bash
npx opensrc <package> # npm package (e.g., npx opensrc zod)
Expand Down
2 changes: 1 addition & 1 deletion app/components/size-badge-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export function SizeBadgePopover({
</AvatarFallback>
</Avatar>
)}
{feedbackName} · {dayjs(feedbackAt).fromNow()}
{feedbackName} · {dayjs.utc(feedbackAt).fromNow()}
</span>
)}
<Button
Expand Down
19 changes: 17 additions & 2 deletions app/libs/business-hours.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ export const calculateBusinessHours = (
end: string,
timezone = 'Asia/Tokyo',
): number => {
const startDate = dayjs(start).tz(timezone)
const endDate = dayjs(end).tz(timezone)
const startDate = dayjs.utc(start).tz(timezone)
const endDate = dayjs.utc(end).tz(timezone)

if (!startDate.isBefore(endDate)) return 0

Expand Down Expand Up @@ -127,3 +127,18 @@ export const calculateBusinessHours = (

return totalHours
}

/**
* 2つの日時間の差を日数で返す(営業時間 or カレンダー時間)
*/
export const diffInDays = (
start: string,
end: string,
businessDaysOnly: boolean,
timezone = 'Asia/Tokyo',
): number => {
const hours = businessDaysOnly
? calculateBusinessHours(start, end, timezone)
: dayjs.utc(end).diff(dayjs.utc(start), 'hour', true)
return hours / 24
}
4 changes: 2 additions & 2 deletions app/routes/$orgSlug/+components/pr-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function getSizeColor(complexity: string | null): BlockColor {
}

function getAgeColor(createdAt: string): BlockColor {
const days = dayjs().diff(dayjs(createdAt), 'day', true)
const days = dayjs().diff(dayjs.utc(createdAt), 'day', true)
for (const t of AGE_THRESHOLDS) {
if (days < t.maxDays) return { bg: t.bg, ring: t.ring, bgFaint: t.bgFaint }
}
Expand Down Expand Up @@ -126,7 +126,7 @@ export function PRPopoverContent({
showAuthor?: boolean
reviewState?: string
}) {
const ageDays = Math.floor(dayjs().diff(dayjs(pr.createdAt), 'day', true))
const ageDays = Math.floor(dayjs().diff(dayjs.utc(pr.createdAt), 'day', true))
const stateInfo = reviewState ? REVIEW_STATE_STYLE[reviewState] : null
return (
<div className="space-y-1">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export const feedbackColumns: ColumnDef<FeedbackRow, unknown>[] = [
header: 'Updated',
cell: (info) => (
<span className="text-muted-foreground text-sm text-nowrap">
{dayjs(info.getValue()).fromNow()}
{dayjs.utc(info.getValue()).fromNow()}
</span>
),
enableSorting: true,
Expand Down
2 changes: 1 addition & 1 deletion app/routes/$orgSlug/analysis/feedbacks/_index/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const loader = async ({ request, context }: Route.LoaderArgs) => {
const sinceDate =
periodMonths === 'all'
? '2000-01-01T00:00:00.000Z'
: dayjs().subtract(periodMonths, 'month').startOf('day').toISOString()
: dayjs.utc().subtract(periodMonths, 'month').startOf('day').toISOString()

const page = Number(url.searchParams.get('page') || '1')
const perPage = Number(url.searchParams.get('per_page') || '20')
Expand Down
2 changes: 1 addition & 1 deletion app/routes/$orgSlug/analysis/reviews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const loader = async ({ request, context }: Route.LoaderArgs) => {
const sinceDate =
periodMonths === 'all'
? '2000-01-01T00:00:00.000Z'
: dayjs().subtract(periodMonths, 'month').startOf('day').toISOString()
: dayjs.utc().subtract(periodMonths, 'month').startOf('day').toISOString()

const teams = await listTeams(organization.id)

Expand Down
3 changes: 2 additions & 1 deletion app/routes/$orgSlug/settings/data-management/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ function RefreshSection({
<Alert>
<AlertDescription>
Scheduled at{' '}
{dayjs(refreshRequestedAt)
{dayjs
.utc(refreshRequestedAt)
.tz(timezone)
.format('YYYY-MM-DD HH:mm:ss')}
. It will run on the next crawl job.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const RepositoryItem = ({
)}
<div className="text-muted-foreground">·</div>
<div className="text-muted-foreground text-xs">
{dayjs(repo.pushedAt).fromNow()}
{dayjs.utc(repo.pushedAt).fromNow()}
</div>

<div className="flex-1" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { pipe, sortBy } from 'remeda'
import { calculateBusinessHours } from '~/app/libs/business-hours'
import dayjs from '~/app/libs/dayjs'
import { diffInDays } from '~/app/libs/business-hours'
import { excludeBots } from '~/app/libs/tenant-query.server'
import { getTenantDb } from '~/app/services/tenant-db.server'
import type { OrganizationId } from '~/app/types/organization'
Expand Down Expand Up @@ -73,23 +72,15 @@ export const getDeployedPullRequestReport = async (
return pipe(
pullrequests.map((pr) => {
const createAndDeployDiff = pr.releasedAt
? (businessDaysOnly
? calculateBusinessHours(
pr.firstCommittedAt ?? pr.pullRequestCreatedAt,
pr.releasedAt,
)
: dayjs(pr.releasedAt).diff(
dayjs(pr.firstCommittedAt ?? pr.pullRequestCreatedAt),
'hour',
true,
)) / 24
? diffInDays(
pr.firstCommittedAt ?? pr.pullRequestCreatedAt,
pr.releasedAt,
businessDaysOnly,
)
: null
const deployTime =
pr.mergedAt && pr.releasedAt
? (businessDaysOnly
? calculateBusinessHours(pr.mergedAt, pr.releasedAt)
: dayjs(pr.releasedAt).diff(dayjs(pr.mergedAt), 'hour', true)) /
24
? diffInDays(pr.mergedAt, pr.releasedAt, businessDaysOnly)
: null
const achievement =
createAndDeployDiff !== null ? createAndDeployDiff < objective : false
Expand Down
18 changes: 6 additions & 12 deletions app/routes/$orgSlug/throughput/merged/+functions/queries.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { pipe, sortBy } from 'remeda'
import { calculateBusinessHours } from '~/app/libs/business-hours'
import dayjs from '~/app/libs/dayjs'
import { diffInDays } from '~/app/libs/business-hours'
import { excludeBots } from '~/app/libs/tenant-query.server'
import { getTenantDb } from '~/app/services/tenant-db.server'
import type { OrganizationId } from '~/app/types/organization'
Expand Down Expand Up @@ -71,16 +70,11 @@ export const getMergedPullRequestReport = async (
return pipe(
pullrequests.map((pr) => {
const createAndMergeDiff = pr.mergedAt
? (businessDaysOnly
? calculateBusinessHours(
pr.firstCommittedAt ?? pr.pullRequestCreatedAt,
pr.mergedAt,
)
: dayjs(pr.mergedAt).diff(
dayjs(pr.firstCommittedAt ?? pr.pullRequestCreatedAt),
'hour',
true,
)) / 24
? diffInDays(
pr.firstCommittedAt ?? pr.pullRequestCreatedAt,
pr.mergedAt,
businessDaysOnly,
)
: null
const achievement =
createAndMergeDiff !== null ? createAndMergeDiff < objective : false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { pipe, sortBy } from 'remeda'
import { calculateBusinessHours } from '~/app/libs/business-hours'
import { diffInDays } from '~/app/libs/business-hours'
import dayjs from '~/app/libs/dayjs'
import { excludeBots } from '~/app/libs/tenant-query.server'
import { getTenantDb } from '~/app/services/tenant-db.server'
Expand Down Expand Up @@ -71,17 +71,14 @@ export const getOngoingPullRequestReport = async (
])
.execute()

const now = new Date().toISOString()
const now = dayjs.utc().toISOString()

return pipe(
pullrequests.map((pr) => {
const startDate = pr.firstCommittedAt ?? pr.pullRequestCreatedAt
const diffHours = businessDaysOnly
? calculateBusinessHours(startDate, now)
: dayjs(now).diff(dayjs(startDate), 'hour', true)
return {
...pr,
createAndNowDiff: diffHours / 24,
createAndNowDiff: diffInDays(startDate, now, businessDaysOnly),
}
}),
sortBy((pr) => (pr.createAndNowDiff ? -pr.createAndNowDiff : 0)),
Expand Down
18 changes: 11 additions & 7 deletions app/routes/$orgSlug/workload/$login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,16 +168,19 @@ export default function MemberWeeklyPage({
holiday: holidays[dateStr] ?? null,
created: createdPRs.filter(
(pr) =>
dayjs(pr.pullRequestCreatedAt).tz(timezone).format('YYYY-MM-DD') ===
dateStr,
dayjs
.utc(pr.pullRequestCreatedAt)
.tz(timezone)
.format('YYYY-MM-DD') === dateStr,
),
merged: mergedPRs.filter(
(pr) =>
dayjs(pr.mergedAt).tz(timezone).format('YYYY-MM-DD') === dateStr,
dayjs.utc(pr.mergedAt).tz(timezone).format('YYYY-MM-DD') === dateStr,
),
reviewed: reviews.filter(
(r) =>
dayjs(r.submittedAt).tz(timezone).format('YYYY-MM-DD') === dateStr,
dayjs.utc(r.submittedAt).tz(timezone).format('YYYY-MM-DD') ===
dateStr,
),
}
})
Expand Down Expand Up @@ -421,7 +424,8 @@ export default function MemberWeeklyPage({
title={pr.title}
url={pr.url}
complexity={pr.complexity}
date={dayjs(pr.pullRequestCreatedAt)
date={dayjs
.utc(pr.pullRequestCreatedAt)
.tz(timezone)
.format('M/D HH:mm')}
/>
Expand All @@ -443,7 +447,7 @@ export default function MemberWeeklyPage({
title={pr.title}
url={pr.url}
complexity={pr.complexity}
date={dayjs(pr.mergedAt).tz(timezone).format('M/D HH:mm')}
date={dayjs.utc(pr.mergedAt).tz(timezone).format('M/D HH:mm')}
extra={pr.totalTime ? `${pr.totalTime.toFixed(1)}d` : undefined}
/>
))}
Expand All @@ -465,7 +469,7 @@ export default function MemberWeeklyPage({
url={r.url}
complexity={r.complexity}
author={r.author}
date={dayjs(r.submittedAt).tz(timezone).format('M/D HH:mm')}
date={dayjs.utc(r.submittedAt).tz(timezone).format('M/D HH:mm')}
reviewState={r.state}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function sortBySize(prs: StackPR[]): StackPR[] {
// --- Age mode ---

function getAgeDays(pr: StackPR): number {
return dayjs().diff(dayjs(pr.createdAt), 'day', true)
return dayjs().diff(dayjs.utc(pr.createdAt), 'day', true)
}

function sortByAge(prs: StackPR[]): StackPR[] {
Expand Down
24 changes: 17 additions & 7 deletions batch/bizlogic/cycletime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ export const codingTime = ({
}: codingTimeProps) => {
if (firstCommittedAt && pullRequestCreatedAt) {
return Math.abs(
dayjs(pullRequestCreatedAt).diff(firstCommittedAt, 'days', true),
dayjs
.utc(pullRequestCreatedAt)
.diff(dayjs.utc(firstCommittedAt), 'days', true),
)
}
return null
Expand All @@ -29,11 +31,15 @@ export const pickupTime = ({
}: pickupTimeProps) => {
if (firstReviewedAt) {
return Math.abs(
dayjs(firstReviewedAt).diff(pullRequestCreatedAt, 'days', true),
dayjs
.utc(firstReviewedAt)
.diff(dayjs.utc(pullRequestCreatedAt), 'days', true),
)
}
if (mergedAt) {
return Math.abs(dayjs(mergedAt).diff(pullRequestCreatedAt, 'days', true))
return Math.abs(
dayjs.utc(mergedAt).diff(dayjs.utc(pullRequestCreatedAt), 'days', true),
)
}
return null
}
Expand All @@ -44,7 +50,9 @@ interface reviewTimeProps {
}
export const reviewTime = ({ firstReviewedAt, mergedAt }: reviewTimeProps) => {
if (firstReviewedAt && mergedAt) {
return Math.abs(dayjs(mergedAt).diff(firstReviewedAt, 'days', true))
return Math.abs(
dayjs.utc(mergedAt).diff(dayjs.utc(firstReviewedAt), 'days', true),
)
}
return null
}
Expand All @@ -55,7 +63,9 @@ interface deployTimeProps {
}
export const deployTime = ({ mergedAt, releasedAt }: deployTimeProps) => {
if (mergedAt && releasedAt) {
return Math.abs(dayjs(releasedAt).diff(mergedAt, 'days', true))
return Math.abs(
dayjs.utc(releasedAt).diff(dayjs.utc(mergedAt), 'days', true),
)
}
return null
}
Expand Down Expand Up @@ -83,12 +93,12 @@ export const totalTime = ({
releasedAt,
],
filter((x) => !!x),
sortBy((x) => dayjs(x).unix()),
sortBy((x) => dayjs.utc(x).unix()),
)
const firstTime = first(times)
const lastTime = last(times)
if (firstTime && lastTime) {
return dayjs(lastTime).diff(firstTime, 'days', true)
return dayjs.utc(lastTime).diff(dayjs.utc(firstTime), 'days', true)
}
return null
}
Loading