sync: update from private repo (44d00295)
Some checks failed
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync 2026-06-29 01:51:27 +00:00
parent a6d1879d63
commit 747377bef9
4 changed files with 56 additions and 14 deletions

View File

@ -44,6 +44,13 @@ export function SpaceRail({ selectedId, onSelect }: SpaceRailProps) {
const auth = useAuthState();
const myUserId = auth.mode === 'authenticated' ? auth.user.id : null;
// 認証無効 (no-auth 単独利用) は admin 同等に扱うApp.tsx の isAdmin と同じ規約)。
const isAdmin = auth.mode === 'authenticated' ? auth.user.role === 'admin' : true;
// case スペースの id 集合。個人スペースバッジの「個人バケツ」判定に使う。
const caseSpaceIds = useMemo(
() => new Set((spaces ?? []).filter(s => s.kind === 'case').map(s => s.id)),
[spaces],
);
const sorted = useMemo(() => sortSpacesForRail(spaces ?? []), [spaces]);
// 他ユーザー所有のスペースが一覧に混在するときadmin が全ユーザーのスペースを
@ -95,7 +102,7 @@ export function SpaceRail({ selectedId, onSelect }: SpaceRailProps) {
space={s}
active={s.id === selectedId}
onSelect={onSelect}
runningCount={countRunningTasksForSpace(tasks ?? [], s, myUserId)}
runningCount={countRunningTasksForSpace(tasks ?? [], s, { viewerId: myUserId, isAdmin, caseSpaceIds })}
mine={hasOthersSpaces && myUserId != null && s.ownerId === myUserId}
/>
))}

View File

@ -12,6 +12,10 @@ MAESTRO に入った、ユーザーに関係する主な変更を新しい順に
> 機能に変更があるたび、このページを更新していきます。日付は変更が本番に入ったおおよその時期です。
## 2026-06-29 — 個人ワークスペースの「実行中」バッジを監視範囲とそろえた(管理者)
左のワークスペース一覧に出る緑の「● N 実行中」バッジが、管理者の個人ワークスペースで、監視できる他ユーザーの個人ワークスペースの実行中タスクを数え落としていた不具合を修正しました。「他のメンバー」タブで見える実行中タスクと、バッジの件数が一致するようになりました。一般ユーザーのバッジは従来どおり自分のぶんだけを数えます(→[ワークスペース](./21-workspaces.md))。
## 2026-06-29 — ワーカー / GPU の「使用中」に種別バッジを追加(管理者のみ)
ワーカー一覧の「使用中: …」表示に、いま動いているジョブの**種別バッジ**が付きました。学習reflectionなど通常タスク以外で GPU が埋まっているときに、ユーザー名のとなりに `reflection` のようなタグが出ます。通常タスクagentにはバッジを付けないので、「reflection で埋まっているだけ」を一目で見分けられます。これも管理者にだけ見えます(→[管理者向け](./19-admin.md))。

View File

@ -110,19 +110,38 @@ describe('countRunningTasksForSpace', () => {
expect(countRunningTasksForSpace(tasks, personalSpace)).toBe(2);
});
it('scopes the personal badge to the viewer when viewerId is given (admin sees all rows)', () => {
// admin のリスト API は全件 (1=1) を返すため、owner 絞りが無いと他ユーザーの
// null タスクが個人スペースのバッジに混ざる。viewerId 指定で本人のみ数える。
it('scopes the personal badge to the viewer for non-admins', () => {
// 非 admin: 共有されない限り他人を数えない。本人の null + 本人の個人スペース id のみ。
const tasks = [
task(1, null, '2026-01-01', 'running', 'u1'), // 自分
task(2, null, '2026-01-02', 'running', 'u2'), // 他ユーザー → 除外
task(3, 'personal', '2026-01-03', 'running', 'u1'), // 自分の個人スペース id
];
expect(countRunningTasksForSpace(tasks, personalSpace, 'u1')).toBe(2);
expect(countRunningTasksForSpace(tasks, personalSpace, { viewerId: 'u1' })).toBe(2);
// viewerId 未指定なら従来どおり全件 (no-auth 単独利用)。
expect(countRunningTasksForSpace(tasks, personalSpace)).toBe(3);
});
it('counts the whole personal bucket for admins (monitoring), incl. other users\' concrete personal ids', () => {
// admin の個人スペースバッジは「他のメンバー」タブと同じ母集団を数える。
// 個人バケツ = null または case でない space_id(= 誰かの個人スペース)。
const tasks = [
task(1, null, '2026-01-01', 'running', 'u1'), // 自分 legacy null
task(2, 'personal', '2026-01-02', 'running', 'u1'), // 自分の個人スペース id
task(3, 'personal-u2', '2026-01-03', 'running', 'u2'), // 他ユーザーの個人スペース(具体 id)
task(4, 'sp-a', '2026-01-04', 'running', 'u2'), // case スペース → 除外
task(5, 'personal-u3', '2026-01-05', 'queued', 'u3'), // running でない → 除外
];
const caseSpaceIds = new Set(['sp-a']);
expect(
countRunningTasksForSpace(tasks, personalSpace, { viewerId: 'u1', isAdmin: true, caseSpaceIds }),
).toBe(3);
// 同じ集合でも非 admin なら本人(u1)のみ。
expect(
countRunningTasksForSpace(tasks, personalSpace, { viewerId: 'u1', isAdmin: false, caseSpaceIds }),
).toBe(2);
});
it('ignores tasks with no latestJob', () => {
const tasks = [task(1, 'sp-a', '2026-01-01'), task(2, 'sp-a', '2026-01-02', 'running')];
expect(countRunningTasksForSpace(tasks, caseSpace)).toBe(1);

View File

@ -34,25 +34,37 @@ export function filterTasksForSpace(
/**
* (latestJob.status === 'running')
* null space_id (filterTasksForSpace )
* API
*
* ****admin API
* (1=1) owner null
* `viewerId` owner
* (no-auth )
* ****:
* - : `space_id === space.id`
* - 個人スペース: 個人バケツ`space_id == null` case space_id =
* `filterTasksForSpace`
* - admin: 本人所有のみ
* - admin: 個人バケツの全員ぶん
*
* `opts.caseSpaceIds` case id
* or null`opts.isAdmin` false / `opts.viewerId`
* (no-auth ) owner
*/
export function countRunningTasksForSpace(
tasks: LocalTask[],
space: { id: string; kind: 'personal' | 'case' },
viewerId?: string | null,
opts?: { viewerId?: string | null; isAdmin?: boolean; caseSpaceIds?: ReadonlySet<string> },
): number {
const isPersonal = space.kind === 'personal';
const viewerId = opts?.viewerId ?? null;
const isAdmin = opts?.isAdmin ?? false;
const caseSpaceIds = opts?.caseSpaceIds;
return tasks.filter((t) => {
if (t.latestJob?.status !== 'running') return false;
const inSpace = t.spaceId === space.id || (isPersonal && t.spaceId == null);
if (!inSpace) return false;
if (isPersonal && viewerId != null && t.ownerId !== viewerId) return false;
if (!isPersonal) return t.spaceId === space.id;
const inBucket =
t.spaceId == null ||
(caseSpaceIds ? !caseSpaceIds.has(t.spaceId) : t.spaceId === space.id);
if (!inBucket) return false;
// 個人スペース: 非adminは本人のみ。admin は監視のため個人バケツ全員を数える。
if (!isAdmin && viewerId != null && t.ownerId !== viewerId) return false;
return true;
}).length;
}