Skip to content

Commit 0e989e5

Browse files
Merge issue-204-slice-1: own visible feedback in workspace surfaces
2 parents fdfb915 + eb41843 commit 0e989e5

18 files changed

Lines changed: 3452 additions & 266 deletions

backend/e2e/canonical-capability-behavior.spec.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1181,15 +1181,18 @@ test.describe('issue 159 retained canonical capability behavior', () => {
11811181

11821182
await adminPage.getByRole('button', { name: 'Add user' }).click();
11831183
await adminPage.getByRole('button', { name: 'Create user' }).click();
1184-
await expect(adminPage.getByRole('status')).toContainText('Name and email are required');
1184+
// Validation is owned by the fields it is about (#204 slice 1).
1185+
await expect(adminPage.locator('.ops-user-form .field-error').first()).toContainText('Name is required.');
1186+
await expect(adminPage.getByLabel('Name')).toHaveAttribute('aria-invalid', 'true');
11851187
await adminPage.getByRole('button', { name: 'Cancel' }).click();
11861188
await setFaults(admin.request, [{ method: 'POST', path: '/api/users', status: 503 }]);
11871189
await adminPage.getByRole('button', { name: 'Add user' }).click();
11881190
await adminPage.getByLabel('Name').fill('Synthetic Failed User');
11891191
await adminPage.getByLabel('Email').fill(`${unique('failed')}@example.invalid`);
11901192
await adminPage.getByLabel('Password').fill('synthetic-password');
11911193
await adminPage.getByRole('button', { name: 'Create user' }).click();
1192-
await expect(adminPage.getByRole('status')).toContainText('Synthetic route failure (503)');
1194+
await expect(adminPage.locator('.ops-user-form-result .form-feedback-error')).toContainText('Synthetic route failure (503)');
1195+
await expect(adminPage.getByLabel('Name')).toHaveValue('Synthetic Failed User');
11931196
await clearFaults(admin.request);
11941197

11951198
const operatorPage = await operator.newPage();
@@ -1577,7 +1580,10 @@ test.describe('issue 159 retained canonical capability behavior', () => {
15771580
await expect(recurringRow).toBeVisible();
15781581
await setFaults(admin.request, [{ method: 'PUT', path: `/api/recurring/${recurring.id}`, status: 503 }]);
15791582
await recurringRow.getByRole('button', { name: 'Pause' }).click();
1580-
await expect(page.locator('#status-text')).toContainText('Could not update recurring operation: Synthetic route failure (503)');
1583+
// The failure belongs to the row whose control was used, not to a hidden
1584+
// shell status line (#204 slice 1).
1585+
await expect(recurringRow.locator('.recurring-row-error')).toContainText('Could not pause this schedule: Synthetic route failure (503)');
1586+
await expect(recurringRow.locator('.recurring-row-error')).toContainText('Select Pause to retry.');
15811587
await clearFaults(admin.request);
15821588
await recurringRow.getByRole('button', { name: 'Pause' }).click();
15831589
await expect(recurringSection.locator('.ops-recurring-item', { hasText: `Synthetic recurring ${id}` }).getByRole('button', { name: 'Resume' })).toBeVisible();

backend/e2e/runtime-template-readonly.spec.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@ test.describe('Git-authored runtime templates', () => {
5151
});
5252
await page.goto('/#/templates');
5353
await expect(page.getByText('Runtime templates unavailable')).toBeVisible();
54-
await expect(page.getByText('Synthetic route failure (503)')).toBeVisible();
54+
await expect(page.locator('.runtime-template-inspector')).toContainText('Synthetic route failure (503)');
55+
// The Templates section also states its own load outcome (#204 slice 1).
56+
const templatesSummary = page.locator('[data-summary-id="tasks-templates"]');
57+
await expect(templatesSummary).toHaveAttribute('data-summary-state', /partial|unavailable/);
58+
await expect(templatesSummary.locator('.surface-summary-detail')).toHaveText('Synthetic route failure (503)');
5559
await request.delete('/__e2e__/route-faults');
5660

5761
await page.goto('/#/templates?templateId=missing-git-template');

backend/e2e/surface-feedback-states.spec.js

Lines changed: 347 additions & 0 deletions
Large diffs are not rendered by default.

frontend/src/runtime/application.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ const {
345345
formatHomeCalendarDate,
346346
formatHomeTaskTiming,
347347
isActiveWorkCard,
348+
getActiveWorkspaceRouteToken,
348349
isOpenWorkTask,
349350
isOperationsHomeVisible,
350351
isWorkflowTemplateDoc,
@@ -372,7 +373,6 @@ const {
372373
resolveCardLabel: (...args) => resolveCardLabel(...args),
373374
resolveDocReference,
374375
setRouteTitle,
375-
setStatus,
376376
settledPayload,
377377
state: homeSurfaceState,
378378
summarizeWorkflowTemplate,
@@ -396,6 +396,8 @@ const {
396396
currentOperatorIdFromPayload,
397397
documentList,
398398
getActiveWorkspaceView: () => workspaceState.activeWorkspaceView,
399+
getActiveWorkspaceRouteToken,
400+
isWorkspaceRouteFresh,
399401
getOperationsQualitySnapshot: () => workspaceState.qualitySnapshot,
400402
getOperationsRecurringSnapshot: () => workspaceState.recurringSnapshot,
401403
getOperationsWorkSnapshot: () => workspaceState.workSnapshot,
@@ -406,13 +408,10 @@ const {
406408
renderSurfaceHeader,
407409
request,
408410
setRouteTitle,
409-
setStatus,
410411
settledPayload,
411412
showCreate,
412-
showErrorToast,
413413
showWorkspaceSurface,
414414
surfaceDescription,
415-
surfaceStatusText,
416415
usersFromWorkPayload,
417416
workApiUrl,
418417
});
@@ -478,7 +477,6 @@ const {
478477
request,
479478
resolveDocReference,
480479
scheduleAnimationFrame: (callback) => requestAnimationFrame(callback),
481-
setStatus,
482480
settledPayload,
483481
showUndoToast,
484482
state: workDetailState,
@@ -559,15 +557,12 @@ const {
559557
renderHonestState,
560558
renderOperationsRuntimeState,
561559
renderSurfaceHeader,
562-
reportError,
563560
resolveAssigneeLabel,
564561
request,
565562
scheduleAnimationFrame: (callback) => requestAnimationFrame(callback),
566563
setRouteTitle,
567-
setStatus,
568564
setWorkspaceEntityState,
569565
shellBody: body,
570-
showErrorToast,
571566
sortWorkTasks,
572567
state: tasksSurfaceState,
573568
surfaceDescription,

frontend/src/styles.css

Lines changed: 215 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1507,6 +1507,12 @@ body[data-workspace-view="tasks"] .document-list.is-operations-home {
15071507
padding: 14px 16px;
15081508
}
15091509

1510+
.home-attention-empty {
1511+
display: grid;
1512+
gap: 6px;
1513+
justify-items: start;
1514+
}
1515+
15101516
.ops-runtime-state {
15111517
display: grid;
15121518
gap: 4px;
@@ -1523,6 +1529,147 @@ body[data-workspace-view="tasks"] .document-list.is-operations-home {
15231529
overflow-wrap: anywhere;
15241530
}
15251531

1532+
/* Owning-surface feedback (#204): each surface states its own load, pending,
1533+
validation and failure outcomes where the operator is already looking. */
1534+
.surface-summary {
1535+
display: grid;
1536+
gap: 6px;
1537+
justify-items: start;
1538+
width: min(var(--content-width), 100%);
1539+
min-width: 0;
1540+
border: 1px solid var(--border-muted);
1541+
border-radius: var(--radius);
1542+
background: var(--surface-muted);
1543+
padding: 10px 14px;
1544+
}
1545+
1546+
.surface-summary-line {
1547+
display: flex;
1548+
flex-wrap: wrap;
1549+
align-items: baseline;
1550+
gap: 8px;
1551+
margin: 0;
1552+
min-width: 0;
1553+
color: var(--text-primary);
1554+
font-size: 14px;
1555+
overflow-wrap: anywhere;
1556+
}
1557+
1558+
.surface-summary-state {
1559+
border: 1px solid var(--border-muted);
1560+
border-radius: 999px;
1561+
background: var(--surface-bg);
1562+
color: var(--text-muted);
1563+
padding: 1px 8px;
1564+
font-size: 12px;
1565+
font-weight: 600;
1566+
text-transform: uppercase;
1567+
letter-spacing: 0.04em;
1568+
}
1569+
1570+
.surface-summary[data-summary-state="unavailable"] {
1571+
border-color: var(--danger-border);
1572+
}
1573+
1574+
.surface-summary[data-summary-state="partial"] {
1575+
border-color: var(--warning-border);
1576+
}
1577+
1578+
.surface-summary-detail {
1579+
color: var(--text-muted);
1580+
font-size: 12px;
1581+
overflow-wrap: anywhere;
1582+
}
1583+
1584+
.surface-summary-retry,
1585+
.quick-form-retry {
1586+
display: inline-flex;
1587+
align-items: center;
1588+
justify-content: center;
1589+
min-height: 32px;
1590+
border: 1px solid var(--control-border);
1591+
border-radius: var(--radius);
1592+
background: var(--control-bg);
1593+
color: var(--text-primary);
1594+
padding: 4px 12px;
1595+
font-size: 13px;
1596+
font-weight: 600;
1597+
}
1598+
1599+
.surface-summary-retry[disabled],
1600+
.quick-form-retry[disabled] {
1601+
opacity: 0.7;
1602+
}
1603+
1604+
/* The Home strip lays its counts out in three columns; the summary explains
1605+
all three, so it spans them instead of taking one count's place. */
1606+
.home-status-strip > .surface-summary {
1607+
grid-column: 1 / -1;
1608+
border: 0;
1609+
border-bottom: 1px solid var(--border-muted);
1610+
border-radius: 0;
1611+
background: transparent;
1612+
width: 100%;
1613+
}
1614+
1615+
.form-feedback {
1616+
display: grid;
1617+
gap: 4px;
1618+
min-width: 0;
1619+
}
1620+
1621+
.form-feedback-status,
1622+
.form-feedback-error,
1623+
.field-error {
1624+
margin: 0;
1625+
min-width: 0;
1626+
font-size: 13px;
1627+
overflow-wrap: anywhere;
1628+
}
1629+
1630+
.form-feedback-status {
1631+
color: var(--text-muted);
1632+
}
1633+
1634+
.form-feedback-error,
1635+
.field-error,
1636+
.ops-user-row-error,
1637+
.recurring-row-error {
1638+
color: var(--danger-text);
1639+
}
1640+
1641+
.field-error {
1642+
display: block;
1643+
margin-top: 4px;
1644+
font-weight: 600;
1645+
}
1646+
1647+
.ops-users-outcome {
1648+
margin: 0;
1649+
color: var(--text-primary);
1650+
font-size: 14px;
1651+
font-weight: 600;
1652+
}
1653+
1654+
.ops-user-row-error {
1655+
margin: 6px 0 0;
1656+
font-size: 13px;
1657+
}
1658+
1659+
.ops-admin-diagnostics-summary {
1660+
margin: 0;
1661+
color: var(--text-muted);
1662+
font-size: 13px;
1663+
}
1664+
1665+
@media (max-width: 768px) {
1666+
.surface-summary-retry,
1667+
.quick-form-retry {
1668+
min-height: 44px;
1669+
padding-inline: 16px;
1670+
}
1671+
}
1672+
15261673
/* Process document availability: one shared outage/empty state. */
15271674
.ops-docs-state {
15281675
display: grid;
@@ -3353,7 +3500,7 @@ body.dark .danger-button {
33533500
/* Recurring create/edit dialog. */
33543501
.recurring-form-intro,
33553502
.recurring-form-preview,
3356-
.recurring-form-error {
3503+
.recurring-form-feedback {
33573504
margin: 0;
33583505
font-size: 13px;
33593506
}
@@ -3371,7 +3518,7 @@ body.dark .danger-button {
33713518
font-weight: 600;
33723519
}
33733520

3374-
.recurring-form-error {
3521+
.recurring-form-feedback .form-feedback-error {
33753522
color: var(--danger-text);
33763523
font-weight: 600;
33773524
}
@@ -6508,13 +6655,13 @@ body[data-workspace-view="calendar"] .document-list {
65086655

65096656
.ops-user-form-result {
65106657
min-height: 20px;
6511-
margin: 0;
6512-
color: var(--success-text);
6513-
font-size: 13px;
6658+
grid-column: 1 / -1;
65146659
}
65156660

6516-
.ops-user-form-result.ops-error {
6517-
color: var(--danger-text);
6661+
.ops-user-form-result
6662+
.form-feedback[data-feedback-state="success"]
6663+
.form-feedback-status {
6664+
color: var(--success-text);
65186665
}
65196666

65206667
.device-panel {
@@ -9060,6 +9207,34 @@ body[data-workspace-view="tasks"] .recurring-form-footer button {
90609207
min-height: 42px;
90619208
}
90629209

9210+
/* Slice 1 controls must remain operable at phone widths without enlarging
9211+
desktop density or unrelated surfaces. */
9212+
@media (max-width: 768px) {
9213+
.ops-surface-recurring .recurring-action,
9214+
.ops-surface-users .primary-button,
9215+
.ops-users-table .ops-user-actions .quiet-button,
9216+
.ops-user-form input:not([type="checkbox"]),
9217+
.ops-user-form select,
9218+
.ops-user-form-actions button,
9219+
.quick-form-overlay .quiet-button,
9220+
.quick-form-label input:not([type="checkbox"]),
9221+
.quick-form-label select,
9222+
.quick-form > .task-action-btn,
9223+
.recurring-form-footer button {
9224+
min-width: 44px;
9225+
min-height: 44px;
9226+
}
9227+
9228+
.quick-form-label.quick-form-checkbox input {
9229+
box-sizing: border-box;
9230+
width: 44px;
9231+
height: 44px;
9232+
min-width: 44px;
9233+
min-height: 44px;
9234+
margin: 0;
9235+
}
9236+
}
9237+
90639238
@keyframes task-state-shimmer {
90649239
0% { background-position: 100% 0; }
90659240
100% { background-position: -100% 0; }
@@ -11257,3 +11432,36 @@ body[data-workspace-view="tasks"] .ops-queue-row strong {
1125711432
overflow-y: auto;
1125811433
}
1125911434
}
11435+
11436+
/* Slice 1 controls must remain operable at phone widths without enlarging
11437+
desktop density or unrelated surfaces. Keep this override after the denser
11438+
Tasks-view rules so equal-specificity mobile declarations cannot lose. */
11439+
@media (max-width: 768px) {
11440+
body[data-workspace-view="tasks"] .ops-surface-recurring .recurring-action,
11441+
body[data-workspace-view="tasks"] .recurring-row-actions .task-action-btn,
11442+
.ops-surface-users .primary-button,
11443+
.ops-users-table .ops-user-actions .quiet-button,
11444+
.ops-user-form input:not([type="checkbox"]),
11445+
.ops-user-form select,
11446+
.ops-user-form-actions button,
11447+
body[data-workspace-view="tasks"] .quick-form-overlay .quiet-button,
11448+
body[data-workspace-view="tasks"] .quick-form-label input:not([type="checkbox"]),
11449+
body[data-workspace-view="tasks"] .quick-form-label select,
11450+
body[data-workspace-view="tasks"] .quick-form > .task-action-btn,
11451+
body[data-workspace-view="tasks"] .recurring-form-footer button,
11452+
.home-quick-action,
11453+
.home-task-action,
11454+
.home-view-all {
11455+
min-width: 44px;
11456+
min-height: 44px;
11457+
}
11458+
11459+
body[data-workspace-view="tasks"] .quick-form-label.quick-form-checkbox input {
11460+
box-sizing: border-box;
11461+
width: 44px;
11462+
height: 44px;
11463+
min-width: 44px;
11464+
min-height: 44px;
11465+
margin: 0;
11466+
}
11467+
}

0 commit comments

Comments
 (0)