Kanban, gantt and timeline lenses over the same task data - #49

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses
Sep 1, 2026
Merged

Kanban, gantt and timeline lenses over the same task data#49
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12

Five new lenses over the existing rows — board, schedule, recent, by_unit on duly_task, and catalog_tree on duly_duty — each reachable from app navigation, plus test/views.test.ts.

Gates

All four green on 997782a, the final commit on this branch:

gateresult
pnpm validateexit 0 — ✓ Validation passed; --json reports warnings: [], conversions: []
pnpm typecheckexit 0
pnpm testexit 0 — Tests 289 passed (289), Test Files 9 passed (9)
pnpm buildexit 0 — ✓ Build complete

The zero-warning validate is the acceptance criterion, not a formality: with the gantt block deleted the same command reports view/layout-without-binding, which is how the reading was confirmed to be a real green rather than an absent check.

The views

board — kanban by status. Columns, their order and their labels come from duly_task.status's own options, so the board cannot drift from the object. Dragging a card writes one field: measured against the renderer's move handler, dataSource.update(object, id, { [groupBy]: toColumnId })status and nothing else, the same write the row action does. That write is not gated on inlineEdit; it is gated on update permission. inlineEdit: true is still declared, and it is not inert — the toolbar lets a viewer switch this view to its grid visualisation, and the adapter honours the flag on that branch.

schedule — gantt, visible_fromdue_date, grouped by owner, week granularity. The span is the lead time. A filter requires both dates to be present, which is a correctness constraint rather than a scope narrowing: the renderer maps a missing date to new Date(), so an undated one-off would draw a zero-width bar on today and read as load that does not exist.

recent — timeline on last_update_at descending, coloured by status. last_update_at is hook-stamped on a status change or a note edit and deliberately does not advance on an administrative write, so this is "what has been happening", not "what has been touched".

by_unit — grid grouped by business_unit, carrying the bulkActionDefs from #41 unchanged. Those are the declarative data-plane write and were copied, not restructured; they are on this view because the adapter honours bulkActionDefs on the grid branch only.

catalog_tree — duties by business unit, then owner. Shipped as type: 'grid' + two-level grouping, not type: 'tree' — see the deviation below.

Navigation: board under My work; schedule / recent / by_unit under Team; catalog_tree under Setup. Every edit appends to an existing children array so #27 can add its entry beside these without a conflict.

board's placement is a product rule, not taste. Dragging a card writes status, and managers do not enter status — assigning is their only write. A drag-to-done surface in the manager's section invites exactly the write the model refuses them. test/views.test.ts pins the placement so it does not migrate.

Status colour

One source of truth: duly_task.status's options[].color. No view carries a colour map, and the test fails if one appears. Every lens that can name a colour source names status.

The gantt is the deliberate exception, and it inverts. Measured on @objectstack/console 17.2.0: the gantt renderer puts record[colorField] straight into backgroundColor, so colorField: 'status' sets background: "open" — invalid CSS, silently dropped, every bar identical. With the key absent the same renderer falls through to its status-derived palette and the bars separate by state. Declaring the documented key is strictly worse than omitting it. The omission is pinned by a test with the reasoning attached so nobody "fixes" it back, and it is filed upstream as objectstack-ai/objectstack#14110.

Deviation: catalog_tree is a grouped grid, not a tree view

The platform's tree view is a self-referencing hierarchy: TreeConfigSchema takes a parentField single-parent pointer and the renderer nests a record under another record of the same object by matching record[parentField] against sibling ids. duly_duty has no self-reference — its lookups point at sys_business_unit, duly_catalog_item and a user — so a type: 'tree' view here resolves no parent for any row, puts every duty at depth 0 and renders a flat table. Not empty: a complete, correct-looking table that never expands. plugin-tree never reads grouping, so declaring the two levels alongside it changes nothing, and no gate says so — view/layout-without-binding does not cover tree at all.

That is precisely the "renders wrong while authoring reports success" shape this card exists to prevent, so it was not shipped. The grid's grouping hook is the honest expression of the same idea and it is measured, not assumed: it recurses through grouping.fields building nested subgroups with per-level collapse — a real two-level hierarchy. Group keys sort by label, never by bucket size.

Filed upstream as objectstack-ai/objectstack#14109 with two suggested resolutions. When one lands, this view can become a real tree.

Platform gaps filed upstream

Six, all measured against this repo on @objectstack/cli 17.2.0 / @objectstack/console 17.2.0, each with the mutation confirmed on disk before the reading and the tree restored after:

upstream issuegap
objectstack-ai/objectstack#14106view/layout-without-binding covers only kanban / calendar / gantt. timeline, tree and map have identical literal-default fallbacks in the renderer ('created_at', 'name', 'location') and no gate. Deleting this PR's timeline block leaves validate reporting warnings: [].
objectstack-ai/objectstack#14107No field reference on a list view is resolved at author time — not columns, filter, sort, grouping, nor any binding block — and pnpm build, the publish gate, does not catch it either. gantt.startDateField: 'B1_no_such_field' builds clean and renders a blank chart.
objectstack-ai/objectstack#14108An app nav viewName naming a view that does not exist silently opens the default view, keeping its authored label.
objectstack-ai/objectstack#14109tree has no value-grouped mode, and misapplying it renders flat rather than empty — the catalog_tree deviation above.
objectstack-ai/objectstack#14110gantt.colorField inverts, as described above. The same key resolves three different ways across plugin-timeline (authored option colours), plugin-calendar (stable hash) and plugin-gantt (raw value).
objectstack-ai/objectstack#14111 (finding)rowColor.colors is optional in the schema but required by the only renderer — rowColor: { field } alone is a parse-clean no-op. This one changed no code here; it is why no view authors rowColor.

The renderer measurements objectstack-ai/objectstack#14074 asked for were posted there rather than filed again.

test/views.test.ts

Two halves, kept apart in the file.

A stopgap for objectstack-ai/objectstack#14106, objectstack-ai/objectstack#14107 and objectstack-ai/objectstack#14108 — binding blocks present and complete for all six binding view types, every field reference on every view surface resolved against the object, every nav viewName resolved against the declared listViews. Written to be deleted when those land, not maintained — the same shape as test/flow-predicates.test.ts for objectstack-ai/objectstack#14089.

Product pins that outlive the platform gaps: the gantt starting at visible_from, the timeline ordering by last_update_at descending, one colour source for status, the gantt's deliberate absent colorField, nothing ordered or totalled by a count, no filter reaching for a derived flag, the board's nav placement.

Every one was reverse-verified — mutate the source, watch the guard turn red, restore:

mutationresult
delete the timeline binding block2 failed / 9 passed
gantt.startDateField → nonexistent field2 failed / 9 passed
add colorField: 'status' to the gantt1 failed / 10 passed
nav viewName typo2 failed / 9 passed
grouping field → nonexistent field1 failed / 10 passed
add a rowColor map1 failed / 10 passed

Files

src/views/task.view.ts, src/views/duty.view.ts, src/apps/duly.app.ts, test/views.test.ts. No other file touched — objectstack.config.ts untouched, bulkActionDefs from #41 unchanged, no changesets in this repo.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 05:25
…y stopgap plus product pins
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kanban, gantt and timeline lenses over the same task data

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Kanban, gantt and timeline lenses over the same task data - #49

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses
Sep 1, 2026
Merged

Kanban, gantt and timeline lenses over the same task data#49
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12

Five new lenses over the existing rows — board, schedule, recent, by_unit on duly_task, and catalog_tree on duly_duty — each reachable from app navigation, plus test/views.test.ts.

Gates

All four green on 997782a, the final commit on this branch:

gateresult
pnpm validateexit 0 — ✓ Validation passed; --json reports warnings: [], conversions: []
pnpm typecheckexit 0
pnpm testexit 0 — Tests 289 passed (289), Test Files 9 passed (9)
pnpm buildexit 0 — ✓ Build complete

The zero-warning validate is the acceptance criterion, not a formality: with the gantt block deleted the same command reports view/layout-without-binding, which is how the reading was confirmed to be a real green rather than an absent check.

The views

board — kanban by status. Columns, their order and their labels come from duly_task.status's own options, so the board cannot drift from the object. Dragging a card writes one field: measured against the renderer's move handler, dataSource.update(object, id, { [groupBy]: toColumnId })status and nothing else, the same write the row action does. That write is not gated on inlineEdit; it is gated on update permission. inlineEdit: true is still declared, and it is not inert — the toolbar lets a viewer switch this view to its grid visualisation, and the adapter honours the flag on that branch.

schedule — gantt, visible_fromdue_date, grouped by owner, week granularity. The span is the lead time. A filter requires both dates to be present, which is a correctness constraint rather than a scope narrowing: the renderer maps a missing date to new Date(), so an undated one-off would draw a zero-width bar on today and read as load that does not exist.

recent — timeline on last_update_at descending, coloured by status. last_update_at is hook-stamped on a status change or a note edit and deliberately does not advance on an administrative write, so this is "what has been happening", not "what has been touched".

by_unit — grid grouped by business_unit, carrying the bulkActionDefs from #41 unchanged. Those are the declarative data-plane write and were copied, not restructured; they are on this view because the adapter honours bulkActionDefs on the grid branch only.

catalog_tree — duties by business unit, then owner. Shipped as type: 'grid' + two-level grouping, not type: 'tree' — see the deviation below.

Navigation: board under My work; schedule / recent / by_unit under Team; catalog_tree under Setup. Every edit appends to an existing children array so #27 can add its entry beside these without a conflict.

board's placement is a product rule, not taste. Dragging a card writes status, and managers do not enter status — assigning is their only write. A drag-to-done surface in the manager's section invites exactly the write the model refuses them. test/views.test.ts pins the placement so it does not migrate.

Status colour

One source of truth: duly_task.status's options[].color. No view carries a colour map, and the test fails if one appears. Every lens that can name a colour source names status.

The gantt is the deliberate exception, and it inverts. Measured on @objectstack/console 17.2.0: the gantt renderer puts record[colorField] straight into backgroundColor, so colorField: 'status' sets background: "open" — invalid CSS, silently dropped, every bar identical. With the key absent the same renderer falls through to its status-derived palette and the bars separate by state. Declaring the documented key is strictly worse than omitting it. The omission is pinned by a test with the reasoning attached so nobody "fixes" it back, and it is filed upstream as objectstack-ai/objectstack#14110.

Deviation: catalog_tree is a grouped grid, not a tree view

The platform's tree view is a self-referencing hierarchy: TreeConfigSchema takes a parentField single-parent pointer and the renderer nests a record under another record of the same object by matching record[parentField] against sibling ids. duly_duty has no self-reference — its lookups point at sys_business_unit, duly_catalog_item and a user — so a type: 'tree' view here resolves no parent for any row, puts every duty at depth 0 and renders a flat table. Not empty: a complete, correct-looking table that never expands. plugin-tree never reads grouping, so declaring the two levels alongside it changes nothing, and no gate says so — view/layout-without-binding does not cover tree at all.

That is precisely the "renders wrong while authoring reports success" shape this card exists to prevent, so it was not shipped. The grid's grouping hook is the honest expression of the same idea and it is measured, not assumed: it recurses through grouping.fields building nested subgroups with per-level collapse — a real two-level hierarchy. Group keys sort by label, never by bucket size.

Filed upstream as objectstack-ai/objectstack#14109 with two suggested resolutions. When one lands, this view can become a real tree.

Platform gaps filed upstream

Six, all measured against this repo on @objectstack/cli 17.2.0 / @objectstack/console 17.2.0, each with the mutation confirmed on disk before the reading and the tree restored after:

upstream issuegap
objectstack-ai/objectstack#14106view/layout-without-binding covers only kanban / calendar / gantt. timeline, tree and map have identical literal-default fallbacks in the renderer ('created_at', 'name', 'location') and no gate. Deleting this PR's timeline block leaves validate reporting warnings: [].
objectstack-ai/objectstack#14107No field reference on a list view is resolved at author time — not columns, filter, sort, grouping, nor any binding block — and pnpm build, the publish gate, does not catch it either. gantt.startDateField: 'B1_no_such_field' builds clean and renders a blank chart.
objectstack-ai/objectstack#14108An app nav viewName naming a view that does not exist silently opens the default view, keeping its authored label.
objectstack-ai/objectstack#14109tree has no value-grouped mode, and misapplying it renders flat rather than empty — the catalog_tree deviation above.
objectstack-ai/objectstack#14110gantt.colorField inverts, as described above. The same key resolves three different ways across plugin-timeline (authored option colours), plugin-calendar (stable hash) and plugin-gantt (raw value).
objectstack-ai/objectstack#14111 (finding)rowColor.colors is optional in the schema but required by the only renderer — rowColor: { field } alone is a parse-clean no-op. This one changed no code here; it is why no view authors rowColor.

The renderer measurements objectstack-ai/objectstack#14074 asked for were posted there rather than filed again.

test/views.test.ts

Two halves, kept apart in the file.

A stopgap for objectstack-ai/objectstack#14106, objectstack-ai/objectstack#14107 and objectstack-ai/objectstack#14108 — binding blocks present and complete for all six binding view types, every field reference on every view surface resolved against the object, every nav viewName resolved against the declared listViews. Written to be deleted when those land, not maintained — the same shape as test/flow-predicates.test.ts for objectstack-ai/objectstack#14089.

Product pins that outlive the platform gaps: the gantt starting at visible_from, the timeline ordering by last_update_at descending, one colour source for status, the gantt's deliberate absent colorField, nothing ordered or totalled by a count, no filter reaching for a derived flag, the board's nav placement.

Every one was reverse-verified — mutate the source, watch the guard turn red, restore:

mutationresult
delete the timeline binding block2 failed / 9 passed
gantt.startDateField → nonexistent field2 failed / 9 passed
add colorField: 'status' to the gantt1 failed / 10 passed
nav viewName typo2 failed / 9 passed
grouping field → nonexistent field1 failed / 10 passed
add a rowColor map1 failed / 10 passed

Files

src/views/task.view.ts, src/views/duty.view.ts, src/apps/duly.app.ts, test/views.test.ts. No other file touched — objectstack.config.ts untouched, bulkActionDefs from #41 unchanged, no changesets in this repo.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 05:25
…y stopgap plus product pins
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kanban, gantt and timeline lenses over the same task data

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Kanban, gantt and timeline lenses over the same task data - #49

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses
Sep 1, 2026
Merged

Kanban, gantt and timeline lenses over the same task data#49
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12

Five new lenses over the existing rows — board, schedule, recent, by_unit on duly_task, and catalog_tree on duly_duty — each reachable from app navigation, plus test/views.test.ts.

Gates

All four green on 997782a, the final commit on this branch:

gateresult
pnpm validateexit 0 — ✓ Validation passed; --json reports warnings: [], conversions: []
pnpm typecheckexit 0
pnpm testexit 0 — Tests 289 passed (289), Test Files 9 passed (9)
pnpm buildexit 0 — ✓ Build complete

The zero-warning validate is the acceptance criterion, not a formality: with the gantt block deleted the same command reports view/layout-without-binding, which is how the reading was confirmed to be a real green rather than an absent check.

The views

board — kanban by status. Columns, their order and their labels come from duly_task.status's own options, so the board cannot drift from the object. Dragging a card writes one field: measured against the renderer's move handler, dataSource.update(object, id, { [groupBy]: toColumnId })status and nothing else, the same write the row action does. That write is not gated on inlineEdit; it is gated on update permission. inlineEdit: true is still declared, and it is not inert — the toolbar lets a viewer switch this view to its grid visualisation, and the adapter honours the flag on that branch.

schedule — gantt, visible_fromdue_date, grouped by owner, week granularity. The span is the lead time. A filter requires both dates to be present, which is a correctness constraint rather than a scope narrowing: the renderer maps a missing date to new Date(), so an undated one-off would draw a zero-width bar on today and read as load that does not exist.

recent — timeline on last_update_at descending, coloured by status. last_update_at is hook-stamped on a status change or a note edit and deliberately does not advance on an administrative write, so this is "what has been happening", not "what has been touched".

by_unit — grid grouped by business_unit, carrying the bulkActionDefs from #41 unchanged. Those are the declarative data-plane write and were copied, not restructured; they are on this view because the adapter honours bulkActionDefs on the grid branch only.

catalog_tree — duties by business unit, then owner. Shipped as type: 'grid' + two-level grouping, not type: 'tree' — see the deviation below.

Navigation: board under My work; schedule / recent / by_unit under Team; catalog_tree under Setup. Every edit appends to an existing children array so #27 can add its entry beside these without a conflict.

board's placement is a product rule, not taste. Dragging a card writes status, and managers do not enter status — assigning is their only write. A drag-to-done surface in the manager's section invites exactly the write the model refuses them. test/views.test.ts pins the placement so it does not migrate.

Status colour

One source of truth: duly_task.status's options[].color. No view carries a colour map, and the test fails if one appears. Every lens that can name a colour source names status.

The gantt is the deliberate exception, and it inverts. Measured on @objectstack/console 17.2.0: the gantt renderer puts record[colorField] straight into backgroundColor, so colorField: 'status' sets background: "open" — invalid CSS, silently dropped, every bar identical. With the key absent the same renderer falls through to its status-derived palette and the bars separate by state. Declaring the documented key is strictly worse than omitting it. The omission is pinned by a test with the reasoning attached so nobody "fixes" it back, and it is filed upstream as objectstack-ai/objectstack#14110.

Deviation: catalog_tree is a grouped grid, not a tree view

The platform's tree view is a self-referencing hierarchy: TreeConfigSchema takes a parentField single-parent pointer and the renderer nests a record under another record of the same object by matching record[parentField] against sibling ids. duly_duty has no self-reference — its lookups point at sys_business_unit, duly_catalog_item and a user — so a type: 'tree' view here resolves no parent for any row, puts every duty at depth 0 and renders a flat table. Not empty: a complete, correct-looking table that never expands. plugin-tree never reads grouping, so declaring the two levels alongside it changes nothing, and no gate says so — view/layout-without-binding does not cover tree at all.

That is precisely the "renders wrong while authoring reports success" shape this card exists to prevent, so it was not shipped. The grid's grouping hook is the honest expression of the same idea and it is measured, not assumed: it recurses through grouping.fields building nested subgroups with per-level collapse — a real two-level hierarchy. Group keys sort by label, never by bucket size.

Filed upstream as objectstack-ai/objectstack#14109 with two suggested resolutions. When one lands, this view can become a real tree.

Platform gaps filed upstream

Six, all measured against this repo on @objectstack/cli 17.2.0 / @objectstack/console 17.2.0, each with the mutation confirmed on disk before the reading and the tree restored after:

upstream issuegap
objectstack-ai/objectstack#14106view/layout-without-binding covers only kanban / calendar / gantt. timeline, tree and map have identical literal-default fallbacks in the renderer ('created_at', 'name', 'location') and no gate. Deleting this PR's timeline block leaves validate reporting warnings: [].
objectstack-ai/objectstack#14107No field reference on a list view is resolved at author time — not columns, filter, sort, grouping, nor any binding block — and pnpm build, the publish gate, does not catch it either. gantt.startDateField: 'B1_no_such_field' builds clean and renders a blank chart.
objectstack-ai/objectstack#14108An app nav viewName naming a view that does not exist silently opens the default view, keeping its authored label.
objectstack-ai/objectstack#14109tree has no value-grouped mode, and misapplying it renders flat rather than empty — the catalog_tree deviation above.
objectstack-ai/objectstack#14110gantt.colorField inverts, as described above. The same key resolves three different ways across plugin-timeline (authored option colours), plugin-calendar (stable hash) and plugin-gantt (raw value).
objectstack-ai/objectstack#14111 (finding)rowColor.colors is optional in the schema but required by the only renderer — rowColor: { field } alone is a parse-clean no-op. This one changed no code here; it is why no view authors rowColor.

The renderer measurements objectstack-ai/objectstack#14074 asked for were posted there rather than filed again.

test/views.test.ts

Two halves, kept apart in the file.

A stopgap for objectstack-ai/objectstack#14106, objectstack-ai/objectstack#14107 and objectstack-ai/objectstack#14108 — binding blocks present and complete for all six binding view types, every field reference on every view surface resolved against the object, every nav viewName resolved against the declared listViews. Written to be deleted when those land, not maintained — the same shape as test/flow-predicates.test.ts for objectstack-ai/objectstack#14089.

Product pins that outlive the platform gaps: the gantt starting at visible_from, the timeline ordering by last_update_at descending, one colour source for status, the gantt's deliberate absent colorField, nothing ordered or totalled by a count, no filter reaching for a derived flag, the board's nav placement.

Every one was reverse-verified — mutate the source, watch the guard turn red, restore:

mutationresult
delete the timeline binding block2 failed / 9 passed
gantt.startDateField → nonexistent field2 failed / 9 passed
add colorField: 'status' to the gantt1 failed / 10 passed
nav viewName typo2 failed / 9 passed
grouping field → nonexistent field1 failed / 10 passed
add a rowColor map1 failed / 10 passed

Files

src/views/task.view.ts, src/views/duty.view.ts, src/apps/duly.app.ts, test/views.test.ts. No other file touched — objectstack.config.ts untouched, bulkActionDefs from #41 unchanged, no changesets in this repo.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 05:25
…y stopgap plus product pins
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kanban, gantt and timeline lenses over the same task data

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Kanban, gantt and timeline lenses over the same task data - #49

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses
Sep 1, 2026
Merged

Kanban, gantt and timeline lenses over the same task data#49
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12

Five new lenses over the existing rows — board, schedule, recent, by_unit on duly_task, and catalog_tree on duly_duty — each reachable from app navigation, plus test/views.test.ts.

Gates

All four green on 997782a, the final commit on this branch:

gateresult
pnpm validateexit 0 — ✓ Validation passed; --json reports warnings: [], conversions: []
pnpm typecheckexit 0
pnpm testexit 0 — Tests 289 passed (289), Test Files 9 passed (9)
pnpm buildexit 0 — ✓ Build complete

The zero-warning validate is the acceptance criterion, not a formality: with the gantt block deleted the same command reports view/layout-without-binding, which is how the reading was confirmed to be a real green rather than an absent check.

The views

board — kanban by status. Columns, their order and their labels come from duly_task.status's own options, so the board cannot drift from the object. Dragging a card writes one field: measured against the renderer's move handler, dataSource.update(object, id, { [groupBy]: toColumnId })status and nothing else, the same write the row action does. That write is not gated on inlineEdit; it is gated on update permission. inlineEdit: true is still declared, and it is not inert — the toolbar lets a viewer switch this view to its grid visualisation, and the adapter honours the flag on that branch.

schedule — gantt, visible_fromdue_date, grouped by owner, week granularity. The span is the lead time. A filter requires both dates to be present, which is a correctness constraint rather than a scope narrowing: the renderer maps a missing date to new Date(), so an undated one-off would draw a zero-width bar on today and read as load that does not exist.

recent — timeline on last_update_at descending, coloured by status. last_update_at is hook-stamped on a status change or a note edit and deliberately does not advance on an administrative write, so this is "what has been happening", not "what has been touched".

by_unit — grid grouped by business_unit, carrying the bulkActionDefs from #41 unchanged. Those are the declarative data-plane write and were copied, not restructured; they are on this view because the adapter honours bulkActionDefs on the grid branch only.

catalog_tree — duties by business unit, then owner. Shipped as type: 'grid' + two-level grouping, not type: 'tree' — see the deviation below.

Navigation: board under My work; schedule / recent / by_unit under Team; catalog_tree under Setup. Every edit appends to an existing children array so #27 can add its entry beside these without a conflict.

board's placement is a product rule, not taste. Dragging a card writes status, and managers do not enter status — assigning is their only write. A drag-to-done surface in the manager's section invites exactly the write the model refuses them. test/views.test.ts pins the placement so it does not migrate.

Status colour

One source of truth: duly_task.status's options[].color. No view carries a colour map, and the test fails if one appears. Every lens that can name a colour source names status.

The gantt is the deliberate exception, and it inverts. Measured on @objectstack/console 17.2.0: the gantt renderer puts record[colorField] straight into backgroundColor, so colorField: 'status' sets background: "open" — invalid CSS, silently dropped, every bar identical. With the key absent the same renderer falls through to its status-derived palette and the bars separate by state. Declaring the documented key is strictly worse than omitting it. The omission is pinned by a test with the reasoning attached so nobody "fixes" it back, and it is filed upstream as objectstack-ai/objectstack#14110.

Deviation: catalog_tree is a grouped grid, not a tree view

The platform's tree view is a self-referencing hierarchy: TreeConfigSchema takes a parentField single-parent pointer and the renderer nests a record under another record of the same object by matching record[parentField] against sibling ids. duly_duty has no self-reference — its lookups point at sys_business_unit, duly_catalog_item and a user — so a type: 'tree' view here resolves no parent for any row, puts every duty at depth 0 and renders a flat table. Not empty: a complete, correct-looking table that never expands. plugin-tree never reads grouping, so declaring the two levels alongside it changes nothing, and no gate says so — view/layout-without-binding does not cover tree at all.

That is precisely the "renders wrong while authoring reports success" shape this card exists to prevent, so it was not shipped. The grid's grouping hook is the honest expression of the same idea and it is measured, not assumed: it recurses through grouping.fields building nested subgroups with per-level collapse — a real two-level hierarchy. Group keys sort by label, never by bucket size.

Filed upstream as objectstack-ai/objectstack#14109 with two suggested resolutions. When one lands, this view can become a real tree.

Platform gaps filed upstream

Six, all measured against this repo on @objectstack/cli 17.2.0 / @objectstack/console 17.2.0, each with the mutation confirmed on disk before the reading and the tree restored after:

upstream issuegap
objectstack-ai/objectstack#14106view/layout-without-binding covers only kanban / calendar / gantt. timeline, tree and map have identical literal-default fallbacks in the renderer ('created_at', 'name', 'location') and no gate. Deleting this PR's timeline block leaves validate reporting warnings: [].
objectstack-ai/objectstack#14107No field reference on a list view is resolved at author time — not columns, filter, sort, grouping, nor any binding block — and pnpm build, the publish gate, does not catch it either. gantt.startDateField: 'B1_no_such_field' builds clean and renders a blank chart.
objectstack-ai/objectstack#14108An app nav viewName naming a view that does not exist silently opens the default view, keeping its authored label.
objectstack-ai/objectstack#14109tree has no value-grouped mode, and misapplying it renders flat rather than empty — the catalog_tree deviation above.
objectstack-ai/objectstack#14110gantt.colorField inverts, as described above. The same key resolves three different ways across plugin-timeline (authored option colours), plugin-calendar (stable hash) and plugin-gantt (raw value).
objectstack-ai/objectstack#14111 (finding)rowColor.colors is optional in the schema but required by the only renderer — rowColor: { field } alone is a parse-clean no-op. This one changed no code here; it is why no view authors rowColor.

The renderer measurements objectstack-ai/objectstack#14074 asked for were posted there rather than filed again.

test/views.test.ts

Two halves, kept apart in the file.

A stopgap for objectstack-ai/objectstack#14106, objectstack-ai/objectstack#14107 and objectstack-ai/objectstack#14108 — binding blocks present and complete for all six binding view types, every field reference on every view surface resolved against the object, every nav viewName resolved against the declared listViews. Written to be deleted when those land, not maintained — the same shape as test/flow-predicates.test.ts for objectstack-ai/objectstack#14089.

Product pins that outlive the platform gaps: the gantt starting at visible_from, the timeline ordering by last_update_at descending, one colour source for status, the gantt's deliberate absent colorField, nothing ordered or totalled by a count, no filter reaching for a derived flag, the board's nav placement.

Every one was reverse-verified — mutate the source, watch the guard turn red, restore:

mutationresult
delete the timeline binding block2 failed / 9 passed
gantt.startDateField → nonexistent field2 failed / 9 passed
add colorField: 'status' to the gantt1 failed / 10 passed
nav viewName typo2 failed / 9 passed
grouping field → nonexistent field1 failed / 10 passed
add a rowColor map1 failed / 10 passed

Files

src/views/task.view.ts, src/views/duty.view.ts, src/apps/duly.app.ts, test/views.test.ts. No other file touched — objectstack.config.ts untouched, bulkActionDefs from #41 unchanged, no changesets in this repo.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 05:25
…y stopgap plus product pins
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kanban, gantt and timeline lenses over the same task data

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Kanban, gantt and timeline lenses over the same task data - #49

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses
Sep 1, 2026
Merged

Kanban, gantt and timeline lenses over the same task data#49
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12

Five new lenses over the existing rows — board, schedule, recent, by_unit on duly_task, and catalog_tree on duly_duty — each reachable from app navigation, plus test/views.test.ts.

Gates

All four green on 997782a, the final commit on this branch:

gateresult
pnpm validateexit 0 — ✓ Validation passed; --json reports warnings: [], conversions: []
pnpm typecheckexit 0
pnpm testexit 0 — Tests 289 passed (289), Test Files 9 passed (9)
pnpm buildexit 0 — ✓ Build complete

The zero-warning validate is the acceptance criterion, not a formality: with the gantt block deleted the same command reports view/layout-without-binding, which is how the reading was confirmed to be a real green rather than an absent check.

The views

board — kanban by status. Columns, their order and their labels come from duly_task.status's own options, so the board cannot drift from the object. Dragging a card writes one field: measured against the renderer's move handler, dataSource.update(object, id, { [groupBy]: toColumnId })status and nothing else, the same write the row action does. That write is not gated on inlineEdit; it is gated on update permission. inlineEdit: true is still declared, and it is not inert — the toolbar lets a viewer switch this view to its grid visualisation, and the adapter honours the flag on that branch.

schedule — gantt, visible_fromdue_date, grouped by owner, week granularity. The span is the lead time. A filter requires both dates to be present, which is a correctness constraint rather than a scope narrowing: the renderer maps a missing date to new Date(), so an undated one-off would draw a zero-width bar on today and read as load that does not exist.

recent — timeline on last_update_at descending, coloured by status. last_update_at is hook-stamped on a status change or a note edit and deliberately does not advance on an administrative write, so this is "what has been happening", not "what has been touched".

by_unit — grid grouped by business_unit, carrying the bulkActionDefs from #41 unchanged. Those are the declarative data-plane write and were copied, not restructured; they are on this view because the adapter honours bulkActionDefs on the grid branch only.

catalog_tree — duties by business unit, then owner. Shipped as type: 'grid' + two-level grouping, not type: 'tree' — see the deviation below.

Navigation: board under My work; schedule / recent / by_unit under Team; catalog_tree under Setup. Every edit appends to an existing children array so #27 can add its entry beside these without a conflict.

board's placement is a product rule, not taste. Dragging a card writes status, and managers do not enter status — assigning is their only write. A drag-to-done surface in the manager's section invites exactly the write the model refuses them. test/views.test.ts pins the placement so it does not migrate.

Status colour

One source of truth: duly_task.status's options[].color. No view carries a colour map, and the test fails if one appears. Every lens that can name a colour source names status.

The gantt is the deliberate exception, and it inverts. Measured on @objectstack/console 17.2.0: the gantt renderer puts record[colorField] straight into backgroundColor, so colorField: 'status' sets background: "open" — invalid CSS, silently dropped, every bar identical. With the key absent the same renderer falls through to its status-derived palette and the bars separate by state. Declaring the documented key is strictly worse than omitting it. The omission is pinned by a test with the reasoning attached so nobody "fixes" it back, and it is filed upstream as objectstack-ai/objectstack#14110.

Deviation: catalog_tree is a grouped grid, not a tree view

The platform's tree view is a self-referencing hierarchy: TreeConfigSchema takes a parentField single-parent pointer and the renderer nests a record under another record of the same object by matching record[parentField] against sibling ids. duly_duty has no self-reference — its lookups point at sys_business_unit, duly_catalog_item and a user — so a type: 'tree' view here resolves no parent for any row, puts every duty at depth 0 and renders a flat table. Not empty: a complete, correct-looking table that never expands. plugin-tree never reads grouping, so declaring the two levels alongside it changes nothing, and no gate says so — view/layout-without-binding does not cover tree at all.

That is precisely the "renders wrong while authoring reports success" shape this card exists to prevent, so it was not shipped. The grid's grouping hook is the honest expression of the same idea and it is measured, not assumed: it recurses through grouping.fields building nested subgroups with per-level collapse — a real two-level hierarchy. Group keys sort by label, never by bucket size.

Filed upstream as objectstack-ai/objectstack#14109 with two suggested resolutions. When one lands, this view can become a real tree.

Platform gaps filed upstream

Six, all measured against this repo on @objectstack/cli 17.2.0 / @objectstack/console 17.2.0, each with the mutation confirmed on disk before the reading and the tree restored after:

upstream issuegap
objectstack-ai/objectstack#14106view/layout-without-binding covers only kanban / calendar / gantt. timeline, tree and map have identical literal-default fallbacks in the renderer ('created_at', 'name', 'location') and no gate. Deleting this PR's timeline block leaves validate reporting warnings: [].
objectstack-ai/objectstack#14107No field reference on a list view is resolved at author time — not columns, filter, sort, grouping, nor any binding block — and pnpm build, the publish gate, does not catch it either. gantt.startDateField: 'B1_no_such_field' builds clean and renders a blank chart.
objectstack-ai/objectstack#14108An app nav viewName naming a view that does not exist silently opens the default view, keeping its authored label.
objectstack-ai/objectstack#14109tree has no value-grouped mode, and misapplying it renders flat rather than empty — the catalog_tree deviation above.
objectstack-ai/objectstack#14110gantt.colorField inverts, as described above. The same key resolves three different ways across plugin-timeline (authored option colours), plugin-calendar (stable hash) and plugin-gantt (raw value).
objectstack-ai/objectstack#14111 (finding)rowColor.colors is optional in the schema but required by the only renderer — rowColor: { field } alone is a parse-clean no-op. This one changed no code here; it is why no view authors rowColor.

The renderer measurements objectstack-ai/objectstack#14074 asked for were posted there rather than filed again.

test/views.test.ts

Two halves, kept apart in the file.

A stopgap for objectstack-ai/objectstack#14106, objectstack-ai/objectstack#14107 and objectstack-ai/objectstack#14108 — binding blocks present and complete for all six binding view types, every field reference on every view surface resolved against the object, every nav viewName resolved against the declared listViews. Written to be deleted when those land, not maintained — the same shape as test/flow-predicates.test.ts for objectstack-ai/objectstack#14089.

Product pins that outlive the platform gaps: the gantt starting at visible_from, the timeline ordering by last_update_at descending, one colour source for status, the gantt's deliberate absent colorField, nothing ordered or totalled by a count, no filter reaching for a derived flag, the board's nav placement.

Every one was reverse-verified — mutate the source, watch the guard turn red, restore:

mutationresult
delete the timeline binding block2 failed / 9 passed
gantt.startDateField → nonexistent field2 failed / 9 passed
add colorField: 'status' to the gantt1 failed / 10 passed
nav viewName typo2 failed / 9 passed
grouping field → nonexistent field1 failed / 10 passed
add a rowColor map1 failed / 10 passed

Files

src/views/task.view.ts, src/views/duty.view.ts, src/apps/duly.app.ts, test/views.test.ts. No other file touched — objectstack.config.ts untouched, bulkActionDefs from #41 unchanged, no changesets in this repo.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 05:25
…y stopgap plus product pins
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kanban, gantt and timeline lenses over the same task data

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Kanban, gantt and timeline lenses over the same task data - #49

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses
Sep 1, 2026
Merged

Kanban, gantt and timeline lenses over the same task data#49
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12

Five new lenses over the existing rows — board, schedule, recent, by_unit on duly_task, and catalog_tree on duly_duty — each reachable from app navigation, plus test/views.test.ts.

Gates

All four green on 997782a, the final commit on this branch:

gateresult
pnpm validateexit 0 — ✓ Validation passed; --json reports warnings: [], conversions: []
pnpm typecheckexit 0
pnpm testexit 0 — Tests 289 passed (289), Test Files 9 passed (9)
pnpm buildexit 0 — ✓ Build complete

The zero-warning validate is the acceptance criterion, not a formality: with the gantt block deleted the same command reports view/layout-without-binding, which is how the reading was confirmed to be a real green rather than an absent check.

The views

board — kanban by status. Columns, their order and their labels come from duly_task.status's own options, so the board cannot drift from the object. Dragging a card writes one field: measured against the renderer's move handler, dataSource.update(object, id, { [groupBy]: toColumnId })status and nothing else, the same write the row action does. That write is not gated on inlineEdit; it is gated on update permission. inlineEdit: true is still declared, and it is not inert — the toolbar lets a viewer switch this view to its grid visualisation, and the adapter honours the flag on that branch.

schedule — gantt, visible_fromdue_date, grouped by owner, week granularity. The span is the lead time. A filter requires both dates to be present, which is a correctness constraint rather than a scope narrowing: the renderer maps a missing date to new Date(), so an undated one-off would draw a zero-width bar on today and read as load that does not exist.

recent — timeline on last_update_at descending, coloured by status. last_update_at is hook-stamped on a status change or a note edit and deliberately does not advance on an administrative write, so this is "what has been happening", not "what has been touched".

by_unit — grid grouped by business_unit, carrying the bulkActionDefs from #41 unchanged. Those are the declarative data-plane write and were copied, not restructured; they are on this view because the adapter honours bulkActionDefs on the grid branch only.

catalog_tree — duties by business unit, then owner. Shipped as type: 'grid' + two-level grouping, not type: 'tree' — see the deviation below.

Navigation: board under My work; schedule / recent / by_unit under Team; catalog_tree under Setup. Every edit appends to an existing children array so #27 can add its entry beside these without a conflict.

board's placement is a product rule, not taste. Dragging a card writes status, and managers do not enter status — assigning is their only write. A drag-to-done surface in the manager's section invites exactly the write the model refuses them. test/views.test.ts pins the placement so it does not migrate.

Status colour

One source of truth: duly_task.status's options[].color. No view carries a colour map, and the test fails if one appears. Every lens that can name a colour source names status.

The gantt is the deliberate exception, and it inverts. Measured on @objectstack/console 17.2.0: the gantt renderer puts record[colorField] straight into backgroundColor, so colorField: 'status' sets background: "open" — invalid CSS, silently dropped, every bar identical. With the key absent the same renderer falls through to its status-derived palette and the bars separate by state. Declaring the documented key is strictly worse than omitting it. The omission is pinned by a test with the reasoning attached so nobody "fixes" it back, and it is filed upstream as objectstack-ai/objectstack#14110.

Deviation: catalog_tree is a grouped grid, not a tree view

The platform's tree view is a self-referencing hierarchy: TreeConfigSchema takes a parentField single-parent pointer and the renderer nests a record under another record of the same object by matching record[parentField] against sibling ids. duly_duty has no self-reference — its lookups point at sys_business_unit, duly_catalog_item and a user — so a type: 'tree' view here resolves no parent for any row, puts every duty at depth 0 and renders a flat table. Not empty: a complete, correct-looking table that never expands. plugin-tree never reads grouping, so declaring the two levels alongside it changes nothing, and no gate says so — view/layout-without-binding does not cover tree at all.

That is precisely the "renders wrong while authoring reports success" shape this card exists to prevent, so it was not shipped. The grid's grouping hook is the honest expression of the same idea and it is measured, not assumed: it recurses through grouping.fields building nested subgroups with per-level collapse — a real two-level hierarchy. Group keys sort by label, never by bucket size.

Filed upstream as objectstack-ai/objectstack#14109 with two suggested resolutions. When one lands, this view can become a real tree.

Platform gaps filed upstream

Six, all measured against this repo on @objectstack/cli 17.2.0 / @objectstack/console 17.2.0, each with the mutation confirmed on disk before the reading and the tree restored after:

upstream issuegap
objectstack-ai/objectstack#14106view/layout-without-binding covers only kanban / calendar / gantt. timeline, tree and map have identical literal-default fallbacks in the renderer ('created_at', 'name', 'location') and no gate. Deleting this PR's timeline block leaves validate reporting warnings: [].
objectstack-ai/objectstack#14107No field reference on a list view is resolved at author time — not columns, filter, sort, grouping, nor any binding block — and pnpm build, the publish gate, does not catch it either. gantt.startDateField: 'B1_no_such_field' builds clean and renders a blank chart.
objectstack-ai/objectstack#14108An app nav viewName naming a view that does not exist silently opens the default view, keeping its authored label.
objectstack-ai/objectstack#14109tree has no value-grouped mode, and misapplying it renders flat rather than empty — the catalog_tree deviation above.
objectstack-ai/objectstack#14110gantt.colorField inverts, as described above. The same key resolves three different ways across plugin-timeline (authored option colours), plugin-calendar (stable hash) and plugin-gantt (raw value).
objectstack-ai/objectstack#14111 (finding)rowColor.colors is optional in the schema but required by the only renderer — rowColor: { field } alone is a parse-clean no-op. This one changed no code here; it is why no view authors rowColor.

The renderer measurements objectstack-ai/objectstack#14074 asked for were posted there rather than filed again.

test/views.test.ts

Two halves, kept apart in the file.

A stopgap for objectstack-ai/objectstack#14106, objectstack-ai/objectstack#14107 and objectstack-ai/objectstack#14108 — binding blocks present and complete for all six binding view types, every field reference on every view surface resolved against the object, every nav viewName resolved against the declared listViews. Written to be deleted when those land, not maintained — the same shape as test/flow-predicates.test.ts for objectstack-ai/objectstack#14089.

Product pins that outlive the platform gaps: the gantt starting at visible_from, the timeline ordering by last_update_at descending, one colour source for status, the gantt's deliberate absent colorField, nothing ordered or totalled by a count, no filter reaching for a derived flag, the board's nav placement.

Every one was reverse-verified — mutate the source, watch the guard turn red, restore:

mutationresult
delete the timeline binding block2 failed / 9 passed
gantt.startDateField → nonexistent field2 failed / 9 passed
add colorField: 'status' to the gantt1 failed / 10 passed
nav viewName typo2 failed / 9 passed
grouping field → nonexistent field1 failed / 10 passed
add a rowColor map1 failed / 10 passed

Files

src/views/task.view.ts, src/views/duty.view.ts, src/apps/duly.app.ts, test/views.test.ts. No other file touched — objectstack.config.ts untouched, bulkActionDefs from #41 unchanged, no changesets in this repo.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 05:25
…y stopgap plus product pins
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kanban, gantt and timeline lenses over the same task data

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Kanban, gantt and timeline lenses over the same task data - #49

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses
Sep 1, 2026
Merged

Kanban, gantt and timeline lenses over the same task data#49
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12

Five new lenses over the existing rows — board, schedule, recent, by_unit on duly_task, and catalog_tree on duly_duty — each reachable from app navigation, plus test/views.test.ts.

Gates

All four green on 997782a, the final commit on this branch:

gateresult
pnpm validateexit 0 — ✓ Validation passed; --json reports warnings: [], conversions: []
pnpm typecheckexit 0
pnpm testexit 0 — Tests 289 passed (289), Test Files 9 passed (9)
pnpm buildexit 0 — ✓ Build complete

The zero-warning validate is the acceptance criterion, not a formality: with the gantt block deleted the same command reports view/layout-without-binding, which is how the reading was confirmed to be a real green rather than an absent check.

The views

board — kanban by status. Columns, their order and their labels come from duly_task.status's own options, so the board cannot drift from the object. Dragging a card writes one field: measured against the renderer's move handler, dataSource.update(object, id, { [groupBy]: toColumnId })status and nothing else, the same write the row action does. That write is not gated on inlineEdit; it is gated on update permission. inlineEdit: true is still declared, and it is not inert — the toolbar lets a viewer switch this view to its grid visualisation, and the adapter honours the flag on that branch.

schedule — gantt, visible_fromdue_date, grouped by owner, week granularity. The span is the lead time. A filter requires both dates to be present, which is a correctness constraint rather than a scope narrowing: the renderer maps a missing date to new Date(), so an undated one-off would draw a zero-width bar on today and read as load that does not exist.

recent — timeline on last_update_at descending, coloured by status. last_update_at is hook-stamped on a status change or a note edit and deliberately does not advance on an administrative write, so this is "what has been happening", not "what has been touched".

by_unit — grid grouped by business_unit, carrying the bulkActionDefs from #41 unchanged. Those are the declarative data-plane write and were copied, not restructured; they are on this view because the adapter honours bulkActionDefs on the grid branch only.

catalog_tree — duties by business unit, then owner. Shipped as type: 'grid' + two-level grouping, not type: 'tree' — see the deviation below.

Navigation: board under My work; schedule / recent / by_unit under Team; catalog_tree under Setup. Every edit appends to an existing children array so #27 can add its entry beside these without a conflict.

board's placement is a product rule, not taste. Dragging a card writes status, and managers do not enter status — assigning is their only write. A drag-to-done surface in the manager's section invites exactly the write the model refuses them. test/views.test.ts pins the placement so it does not migrate.

Status colour

One source of truth: duly_task.status's options[].color. No view carries a colour map, and the test fails if one appears. Every lens that can name a colour source names status.

The gantt is the deliberate exception, and it inverts. Measured on @objectstack/console 17.2.0: the gantt renderer puts record[colorField] straight into backgroundColor, so colorField: 'status' sets background: "open" — invalid CSS, silently dropped, every bar identical. With the key absent the same renderer falls through to its status-derived palette and the bars separate by state. Declaring the documented key is strictly worse than omitting it. The omission is pinned by a test with the reasoning attached so nobody "fixes" it back, and it is filed upstream as objectstack-ai/objectstack#14110.

Deviation: catalog_tree is a grouped grid, not a tree view

The platform's tree view is a self-referencing hierarchy: TreeConfigSchema takes a parentField single-parent pointer and the renderer nests a record under another record of the same object by matching record[parentField] against sibling ids. duly_duty has no self-reference — its lookups point at sys_business_unit, duly_catalog_item and a user — so a type: 'tree' view here resolves no parent for any row, puts every duty at depth 0 and renders a flat table. Not empty: a complete, correct-looking table that never expands. plugin-tree never reads grouping, so declaring the two levels alongside it changes nothing, and no gate says so — view/layout-without-binding does not cover tree at all.

That is precisely the "renders wrong while authoring reports success" shape this card exists to prevent, so it was not shipped. The grid's grouping hook is the honest expression of the same idea and it is measured, not assumed: it recurses through grouping.fields building nested subgroups with per-level collapse — a real two-level hierarchy. Group keys sort by label, never by bucket size.

Filed upstream as objectstack-ai/objectstack#14109 with two suggested resolutions. When one lands, this view can become a real tree.

Platform gaps filed upstream

Six, all measured against this repo on @objectstack/cli 17.2.0 / @objectstack/console 17.2.0, each with the mutation confirmed on disk before the reading and the tree restored after:

upstream issuegap
objectstack-ai/objectstack#14106view/layout-without-binding covers only kanban / calendar / gantt. timeline, tree and map have identical literal-default fallbacks in the renderer ('created_at', 'name', 'location') and no gate. Deleting this PR's timeline block leaves validate reporting warnings: [].
objectstack-ai/objectstack#14107No field reference on a list view is resolved at author time — not columns, filter, sort, grouping, nor any binding block — and pnpm build, the publish gate, does not catch it either. gantt.startDateField: 'B1_no_such_field' builds clean and renders a blank chart.
objectstack-ai/objectstack#14108An app nav viewName naming a view that does not exist silently opens the default view, keeping its authored label.
objectstack-ai/objectstack#14109tree has no value-grouped mode, and misapplying it renders flat rather than empty — the catalog_tree deviation above.
objectstack-ai/objectstack#14110gantt.colorField inverts, as described above. The same key resolves three different ways across plugin-timeline (authored option colours), plugin-calendar (stable hash) and plugin-gantt (raw value).
objectstack-ai/objectstack#14111 (finding)rowColor.colors is optional in the schema but required by the only renderer — rowColor: { field } alone is a parse-clean no-op. This one changed no code here; it is why no view authors rowColor.

The renderer measurements objectstack-ai/objectstack#14074 asked for were posted there rather than filed again.

test/views.test.ts

Two halves, kept apart in the file.

A stopgap for objectstack-ai/objectstack#14106, objectstack-ai/objectstack#14107 and objectstack-ai/objectstack#14108 — binding blocks present and complete for all six binding view types, every field reference on every view surface resolved against the object, every nav viewName resolved against the declared listViews. Written to be deleted when those land, not maintained — the same shape as test/flow-predicates.test.ts for objectstack-ai/objectstack#14089.

Product pins that outlive the platform gaps: the gantt starting at visible_from, the timeline ordering by last_update_at descending, one colour source for status, the gantt's deliberate absent colorField, nothing ordered or totalled by a count, no filter reaching for a derived flag, the board's nav placement.

Every one was reverse-verified — mutate the source, watch the guard turn red, restore:

mutationresult
delete the timeline binding block2 failed / 9 passed
gantt.startDateField → nonexistent field2 failed / 9 passed
add colorField: 'status' to the gantt1 failed / 10 passed
nav viewName typo2 failed / 9 passed
grouping field → nonexistent field1 failed / 10 passed
add a rowColor map1 failed / 10 passed

Files

src/views/task.view.ts, src/views/duty.view.ts, src/apps/duly.app.ts, test/views.test.ts. No other file touched — objectstack.config.ts untouched, bulkActionDefs from #41 unchanged, no changesets in this repo.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 05:25
…y stopgap plus product pins
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kanban, gantt and timeline lenses over the same task data

1 participant

@os-warren
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Kanban, gantt and timeline lenses over the same task data - #49

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses
Sep 1, 2026
Merged

Kanban, gantt and timeline lenses over the same task data#49
os-warren merged 2 commits into
mainfrom
claude/issue-12-view-lenses

Conversation

@os-warren

@os-warrenos-warren commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12

Five new lenses over the existing rows — board, schedule, recent, by_unit on duly_task, and catalog_tree on duly_duty — each reachable from app navigation, plus test/views.test.ts.

Gates

All four green on 997782a, the final commit on this branch:

gateresult
pnpm validateexit 0 — ✓ Validation passed; --json reports warnings: [], conversions: []
pnpm typecheckexit 0
pnpm testexit 0 — Tests 289 passed (289), Test Files 9 passed (9)
pnpm buildexit 0 — ✓ Build complete

The zero-warning validate is the acceptance criterion, not a formality: with the gantt block deleted the same command reports view/layout-without-binding, which is how the reading was confirmed to be a real green rather than an absent check.

The views

board — kanban by status. Columns, their order and their labels come from duly_task.status's own options, so the board cannot drift from the object. Dragging a card writes one field: measured against the renderer's move handler, dataSource.update(object, id, { [groupBy]: toColumnId })status and nothing else, the same write the row action does. That write is not gated on inlineEdit; it is gated on update permission. inlineEdit: true is still declared, and it is not inert — the toolbar lets a viewer switch this view to its grid visualisation, and the adapter honours the flag on that branch.

schedule — gantt, visible_fromdue_date, grouped by owner, week granularity. The span is the lead time. A filter requires both dates to be present, which is a correctness constraint rather than a scope narrowing: the renderer maps a missing date to new Date(), so an undated one-off would draw a zero-width bar on today and read as load that does not exist.

recent — timeline on last_update_at descending, coloured by status. last_update_at is hook-stamped on a status change or a note edit and deliberately does not advance on an administrative write, so this is "what has been happening", not "what has been touched".

by_unit — grid grouped by business_unit, carrying the bulkActionDefs from #41 unchanged. Those are the declarative data-plane write and were copied, not restructured; they are on this view because the adapter honours bulkActionDefs on the grid branch only.

catalog_tree — duties by business unit, then owner. Shipped as type: 'grid' + two-level grouping, not type: 'tree' — see the deviation below.

Navigation: board under My work; schedule / recent / by_unit under Team; catalog_tree under Setup. Every edit appends to an existing children array so #27 can add its entry beside these without a conflict.

board's placement is a product rule, not taste. Dragging a card writes status, and managers do not enter status — assigning is their only write. A drag-to-done surface in the manager's section invites exactly the write the model refuses them. test/views.test.ts pins the placement so it does not migrate.

Status colour

One source of truth: duly_task.status's options[].color. No view carries a colour map, and the test fails if one appears. Every lens that can name a colour source names status.

The gantt is the deliberate exception, and it inverts. Measured on @objectstack/console 17.2.0: the gantt renderer puts record[colorField] straight into backgroundColor, so colorField: 'status' sets background: "open" — invalid CSS, silently dropped, every bar identical. With the key absent the same renderer falls through to its status-derived palette and the bars separate by state. Declaring the documented key is strictly worse than omitting it. The omission is pinned by a test with the reasoning attached so nobody "fixes" it back, and it is filed upstream as objectstack-ai/objectstack#14110.

Deviation: catalog_tree is a grouped grid, not a tree view

The platform's tree view is a self-referencing hierarchy: TreeConfigSchema takes a parentField single-parent pointer and the renderer nests a record under another record of the same object by matching record[parentField] against sibling ids. duly_duty has no self-reference — its lookups point at sys_business_unit, duly_catalog_item and a user — so a type: 'tree' view here resolves no parent for any row, puts every duty at depth 0 and renders a flat table. Not empty: a complete, correct-looking table that never expands. plugin-tree never reads grouping, so declaring the two levels alongside it changes nothing, and no gate says so — view/layout-without-binding does not cover tree at all.

That is precisely the "renders wrong while authoring reports success" shape this card exists to prevent, so it was not shipped. The grid's grouping hook is the honest expression of the same idea and it is measured, not assumed: it recurses through grouping.fields building nested subgroups with per-level collapse — a real two-level hierarchy. Group keys sort by label, never by bucket size.

Filed upstream as objectstack-ai/objectstack#14109 with two suggested resolutions. When one lands, this view can become a real tree.

Platform gaps filed upstream

Six, all measured against this repo on @objectstack/cli 17.2.0 / @objectstack/console 17.2.0, each with the mutation confirmed on disk before the reading and the tree restored after:

upstream issuegap
objectstack-ai/objectstack#14106view/layout-without-binding covers only kanban / calendar / gantt. timeline, tree and map have identical literal-default fallbacks in the renderer ('created_at', 'name', 'location') and no gate. Deleting this PR's timeline block leaves validate reporting warnings: [].
objectstack-ai/objectstack#14107No field reference on a list view is resolved at author time — not columns, filter, sort, grouping, nor any binding block — and pnpm build, the publish gate, does not catch it either. gantt.startDateField: 'B1_no_such_field' builds clean and renders a blank chart.
objectstack-ai/objectstack#14108An app nav viewName naming a view that does not exist silently opens the default view, keeping its authored label.
objectstack-ai/objectstack#14109tree has no value-grouped mode, and misapplying it renders flat rather than empty — the catalog_tree deviation above.
objectstack-ai/objectstack#14110gantt.colorField inverts, as described above. The same key resolves three different ways across plugin-timeline (authored option colours), plugin-calendar (stable hash) and plugin-gantt (raw value).
objectstack-ai/objectstack#14111 (finding)rowColor.colors is optional in the schema but required by the only renderer — rowColor: { field } alone is a parse-clean no-op. This one changed no code here; it is why no view authors rowColor.

The renderer measurements objectstack-ai/objectstack#14074 asked for were posted there rather than filed again.

test/views.test.ts

Two halves, kept apart in the file.

A stopgap for objectstack-ai/objectstack#14106, objectstack-ai/objectstack#14107 and objectstack-ai/objectstack#14108 — binding blocks present and complete for all six binding view types, every field reference on every view surface resolved against the object, every nav viewName resolved against the declared listViews. Written to be deleted when those land, not maintained — the same shape as test/flow-predicates.test.ts for objectstack-ai/objectstack#14089.

Product pins that outlive the platform gaps: the gantt starting at visible_from, the timeline ordering by last_update_at descending, one colour source for status, the gantt's deliberate absent colorField, nothing ordered or totalled by a count, no filter reaching for a derived flag, the board's nav placement.

Every one was reverse-verified — mutate the source, watch the guard turn red, restore:

mutationresult
delete the timeline binding block2 failed / 9 passed
gantt.startDateField → nonexistent field2 failed / 9 passed
add colorField: 'status' to the gantt1 failed / 10 passed
nav viewName typo2 failed / 9 passed
grouping field → nonexistent field1 failed / 10 passed
add a rowColor map1 failed / 10 passed

Files

src/views/task.view.ts, src/views/duty.view.ts, src/apps/duly.app.ts, test/views.test.ts. No other file touched — objectstack.config.ts untouched, bulkActionDefs from #41 unchanged, no changesets in this repo.


Generated by Claude Code

os-warrenand others added 2 commits September 1, 2026 05:25
…y stopgap plus product pins
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqkTcrxUFci7nqXdbBSe2p
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Kanban, gantt and timeline lenses over the same task data

1 participant

@os-warren