Publish SHD MCP plugin package

This commit is contained in:
2026-08-24 23:39:10 +03:00
commit 7f07084699
228 changed files with 24949 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
{
"name": "shd-public",
"interface": {
"displayName": "SHD Public Plugins"
},
"plugins": [
{
"name": "shd-mcp-plugin",
"source": {
"source": "local",
"path": "./plugins/shd-mcp-plugin"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
+9
View File
@@ -0,0 +1,9 @@
.env
.env.*
!.env.example
.DS_Store
.idea/
.vscode/
*.log
__pycache__/
*.py[cod]
+93
View File
@@ -0,0 +1,93 @@
# SHD MCP in ChatGPT
This file describes the user-side connection. The repository cannot create an
app inside another persons ChatGPT account and never stores OAuth tokens.
## Custom app
1. Open ChatGPT settings and enable Developer mode for Apps/Connectors.
2. Create a custom app with this MCP endpoint:
`https://shd.xyz.su/mcp`
3. Complete the SHD OAuth sign-in and consent screen.
4. Start a new chat or refresh the app connection.
5. Test with: `Покажи активные проекты с датой завершения и статусом.`
The server publishes OAuth 2.1 discovery for the root resource and supports
ChatGPT Client ID Metadata Documents (CIMD), PKCE S256 and the
`private_key_jwt` token-endpoint authentication method. Keep the app URL as
`https://shd.xyz.su/mcp`; the retired `/mcp/system-data` resource is not a
valid replacement for the root MCP resource.
The expected flow is:
1. ChatGPT calls `shd_list_projects` with `archived: false`.
2. ChatGPT calls the corresponding module render tool after the list tool
before the final response when MCP Apps UI is supported:
`shd_render_projects_widget`, `shd_render_documents_widget`,
`shd_render_tasks_widget`, `shd_render_finance_widget`,
`shd_render_crm_widget`, `shd_render_discussions_widget`,
`shd_render_notifications_widget`, `shd_render_scheduling_widget`,
`shd_render_inventory_widget`, `shd_render_agents_widget`,
`shd_render_status_page_widget`, `shd_render_project_db_widget` or
`shd_render_notes_widget`, `shd_render_project_overview_widget`,
`shd_render_files_widget`, `shd_render_proposals_widget`,
`shd_render_terms_widget`, `shd_render_activity_widget`,
`shd_render_organizations_widget`, `shd_render_gitea_widget` or
`shd_render_schemes_widget`, `shd_render_task_kanban_widget`,
`shd_render_team_workload_widget`, `shd_render_project_timeline_widget`,
`shd_render_crm_funnel_widget`, `shd_render_finance_dashboard_widget`,
`shd_render_proposal_approvals_widget`, `shd_render_documents_completeness_widget`,
`shd_render_agents_health_widget`, `shd_render_schedule_calendar_widget`,
`shd_render_acl_matrix_widget`, `shd_render_inventory_warnings_widget`,
`shd_render_gitea_board_widget`, `shd_render_schemes_progress_widget`,
`shd_render_notes_tree_widget` or `shd_render_project_db_preview_widget`.
3. The selected widget renders a register, can call the matching detail tool for
one selected row and exposes controlled write actions when declared by the
render metadata.
The widget resources are versioned at `ui://shd/active-projects/v1.html`,
`ui://shd/documents/v1.html`, `ui://shd/tasks/v1.html`,
`ui://shd/finance/v1.html`, `ui://shd/crm/v1.html`,
`ui://shd/discussions/v1.html`, `ui://shd/notifications/v1.html`,
`ui://shd/scheduling/v1.html`, `ui://shd/inventory/v1.html`,
`ui://shd/agents/v1.html`, `ui://shd/status-page/v1.html`,
`ui://shd/project-db/v1.html`, `ui://shd/notes/v1.html`,
`ui://shd/project-overview/v1.html`, `ui://shd/files/v1.html`,
`ui://shd/proposals/v1.html`, `ui://shd/terms/v1.html`,
`ui://shd/activity/v1.html`, `ui://shd/organizations/v1.html`,
`ui://shd/gitea/v1.html`, `ui://shd/schemes/v1.html`,
`ui://shd/task-kanban/v1.html`, `ui://shd/team-workload/v1.html`,
`ui://shd/project-timeline/v1.html`, `ui://shd/crm-funnel/v1.html`,
`ui://shd/finance-dashboard/v1.html`, `ui://shd/proposal-approvals/v1.html`,
`ui://shd/documents-completeness/v1.html`, `ui://shd/agents-health/v1.html`,
`ui://shd/schedule-calendar/v1.html`, `ui://shd/acl-matrix/v1.html`,
`ui://shd/inventory-warnings/v1.html`, `ui://shd/gitea-board/v1.html`,
`ui://shd/schemes-progress/v1.html`, `ui://shd/notes-tree/v1.html` and
`ui://shd/project-db-preview/v1.html`.
The MCP tools remain usable without the widget. A failed OAuth connection is
an account/endpoint configuration problem, not a missing token in this repo.
## Public directory
Directory submission is a separate optional release step. It is needed for
public discovery and reviewed installation, not for internal use or a custom
app added manually in ChatGPT. Before submission, the owner must provide the
production HTTPS endpoint, OAuth metadata, privacy/support URLs, accurate app
metadata, test prompts and any review credentials requested by OpenAI.
Use the official [submission guide](https://developers.openai.com/plugins/deploy/submission)
and [plugin guidelines](https://developers.openai.com/plugins/app-guidelines).
## License choice
This repository currently uses the proprietary SHD MCP Plugin License. Public
visibility means the source can be viewed; it does not grant permission to
redistribute, resell, modify or use it outside an authorized SHD account.
MIT would grant anyone permission to use, copy, modify, publish, sublicense and
sell the plugin, including forks, while retaining only the copyright notice and
license text. Switching to MIT is a product/legal decision and is not done
implicitly by publishing the repository.
+37
View File
@@ -0,0 +1,37 @@
SHD MCP PLUGIN LICENSE
Copyright (c) 2026 SHD
This license applies to the SHD MCP Plugin package, its Skills, manifests,
documentation and SHD branding in this repository. The SHD MCP service and
its backend are separate systems and remain subject to their own terms.
1. Permission
SHD grants a limited, non-exclusive, non-transferable permission to install and
use an unmodified copy of this plugin with an authorized SHD account and a
compatible MCP client.
2. Restrictions
Unless SHD gives prior written permission, you may not sell, sublicense,
redistribute, publish modified versions of, remove notices from, or use this
package and its branding to provide a competing service. Public access to this
repository does not transfer copyright, trademark or other intellectual
property rights.
3. No credentials or access grant
This package contains no access token and grants no access to SHD data. Access
is controlled by SHD authentication, organization membership and server-side
permissions.
4. Warranty and liability
The package is provided "as is" to the maximum extent permitted by law,
without warranties of any kind. SHD is not liable for indirect, incidental or
consequential damages arising from its use.
5. Third-party materials
Third-party software and assets, if any, remain subject to their own licenses.
+162
View File
@@ -0,0 +1,162 @@
# SHD MCP Plugin
Public installable plugin package for SHD project workflows. It combines Codex
Skills with the authenticated SHD MCP server and keeps business logic, ACLs,
OAuth and persisted data in SHD itself.
## What the user installs
The package contains:
- `.codex-plugin/plugin.json` — plugin metadata;
- `.mcp.json` — the official SHD Streamable HTTP MCP endpoint;
- `skills/` — routing, project, ProjectBase, task, file, finance, CRM,
analytics, estimates, scheduling, entity resolution, Wiki, discussions,
documents, organizations/ACL, notifications, inventory, agents, status-page,
realtime/activity, Terms/contracts and Gitea workflows;
- `assets/` — plugin branding;
- `widgets/` — versioned MCP Apps resources with manifest, checksum and
provenance metadata;
- `.agents/plugins/marketplace.json` — a ready local marketplace entry.
It does not contain the SHD Laravel application, database code or credentials.
## Install in Codex
Clone this repository and register its marketplace:
```bash
git clone https://github.com/bulava92/shd-mcp-plugin.git
cd shd-mcp-plugin
codex plugin marketplace add "$PWD"
codex plugin add shd-mcp-plugin@shd-public
```
After installation, start a new Codex thread so the plugin Skills are loaded.
## ChatGPT custom app
ChatGPT uses the same SHD MCP server connection, but it is added separately in
the ChatGPT account: open Settings → Apps/Connectors → Developer mode, create
a custom app and set the MCP URL to `https://shd.xyz.su/mcp`. Complete the SHD
OAuth flow and refresh the app after server metadata changes.
The active-projects result can render an inline MCP Apps widget. It is a small
dashboard with:
- `Проект → Дата завершения → Статус`;
- sorting by the nearest deadline;
- local search by project name/code;
- a refresh action, a project-details panel and controlled project actions.
If the host does not support MCP Apps UI, the data tool and the plain Markdown
table remain fully usable.
The backend serves the checked-in widget artifact at
`ui://shd/active-projects/v1.html`; its manifest and `SHA256SUMS` are kept next
to the HTML resource under `plugins/shd-mcp-plugin/widgets/active-projects/v1/`.
The same data/render contract is available for Documents, Tasks,
Finance, CRM, Discussions, Notifications, Scheduling, Inventory, Agents,
Status Page, Project DB, Notes, Project overview, Files, Proposals, Terms,
Activity, Organizations and access, Gitea Issues and Schemes:
- `shd_render_documents_widget``ui://shd/documents/v1.html`;
- `shd_render_tasks_widget``ui://shd/tasks/v1.html`;
- `shd_render_finance_widget``ui://shd/finance/v1.html`;
- `shd_render_crm_widget``ui://shd/crm/v1.html`;
- `shd_render_discussions_widget``ui://shd/discussions/v1.html`;
- `shd_render_notifications_widget``ui://shd/notifications/v1.html`;
- `shd_render_scheduling_widget``ui://shd/scheduling/v1.html`;
- `shd_render_inventory_widget``ui://shd/inventory/v1.html`;
- `shd_render_agents_widget``ui://shd/agents/v1.html`;
- `shd_render_status_page_widget``ui://shd/status-page/v1.html`;
- `shd_render_project_db_widget``ui://shd/project-db/v1.html`;
- `shd_render_notes_widget``ui://shd/notes/v1.html`.
- `shd_render_project_overview_widget``ui://shd/project-overview/v1.html`;
- `shd_render_files_widget``ui://shd/files/v1.html`;
- `shd_render_proposals_widget``ui://shd/proposals/v1.html`;
- `shd_render_terms_widget``ui://shd/terms/v1.html`;
- `shd_render_activity_widget``ui://shd/activity/v1.html`;
- `shd_render_organizations_widget``ui://shd/organizations/v1.html`;
- `shd_render_gitea_widget``ui://shd/gitea/v1.html`;
- `shd_render_schemes_widget``ui://shd/schemes/v1.html`;
- `shd_render_task_kanban_widget``ui://shd/task-kanban/v1.html`;
- `shd_render_team_workload_widget``ui://shd/team-workload/v1.html`;
- `shd_render_project_timeline_widget``ui://shd/project-timeline/v1.html`;
- `shd_render_crm_funnel_widget``ui://shd/crm-funnel/v1.html`;
- `shd_render_finance_dashboard_widget``ui://shd/finance-dashboard/v1.html`;
- `shd_render_proposal_approvals_widget``ui://shd/proposal-approvals/v1.html`;
- `shd_render_documents_completeness_widget``ui://shd/documents-completeness/v1.html`;
- `shd_render_agents_health_widget``ui://shd/agents-health/v1.html`;
- `shd_render_schedule_calendar_widget``ui://shd/schedule-calendar/v1.html`;
- `shd_render_acl_matrix_widget``ui://shd/acl-matrix/v1.html`;
- `shd_render_inventory_warnings_widget``ui://shd/inventory-warnings/v1.html`;
- `shd_render_gitea_board_widget``ui://shd/gitea-board/v1.html`;
- `shd_render_schemes_progress_widget``ui://shd/schemes-progress/v1.html`;
- `shd_render_notes_tree_widget``ui://shd/notes-tree/v1.html`;
- `shd_render_project_db_preview_widget``ui://shd/project-db-preview/v1.html`.
Each widget accepts normalized records from its list tool, keeps the plain
structured response as a fallback and uses the MCP Apps bridge before the
`window.openai` compatibility API.
The official OpenAI directory is optional. It is useful for public discovery,
one-click installation and review of a public app; it is not required for an
internal team or a manually added ChatGPT custom app. See the official
[MCP server guide](https://developers.openai.com/plugins/build/mcp-server),
[UI guide](https://developers.openai.com/plugins/build/chatgpt-ui), and
[submission guide](https://developers.openai.com/plugins/deploy/submission)
when public listing is the goal.
## Connect SHD
Installation and authorization are separate steps:
1. The client reads `.mcp.json` and opens the SHD OAuth flow.
2. The user signs in to SHD and approves the requested MCP connection.
3. SHD issues the client an authorization; the client stores it securely.
4. Every tool call is checked again by SHD for organization, project and
module permissions.
The user does not need to put a token in this repository. A personal access
token may be used only if the selected MCP client and SHD deployment
explicitly support it; enter it in that client's secure connection settings,
never in `.mcp.json`, Skills or an issue.
SHD OAuth supports ChatGPT CIMD clients with PKCE S256 and
`private_key_jwt` client authentication. The server validates the client
assertion against ChatGPT's published JWKS; the plugin package never stores
the assertion, access token or refresh token.
For ChatGPT or another MCP client without Codex plugin installation, add the
same HTTPS endpoint as a custom MCP app and complete OAuth there. The public
repository packages the workflows; it does not grant access to the server.
## Troubleshooting
- **Plugin installs but tools are unavailable:** reconnect SHD OAuth and start
a new thread.
- **Projects are missing:** the SHD account or organization lacks access, or
the project is archived; verify permissions in SHD.
- **A write is rejected:** the server ACL, required role, version or conflict
check rejected it. Do not bypass the error with a different token.
- **A module is not covered by a dedicated skill:** use `shd-routing`; the MCP
server still exposes the current authenticated tool catalog, while skills
provide focused workflow and safety guidance for common module operations.
- **Another SHD installation is required:** use its approved MCP URL and its
OAuth resource in the client's secure connection configuration.
## Development
Keep changes inside the plugin package. The authenticated MCP server, OAuth
deployment and runtime verification remain private server-side concerns. Do not
place credentials, private host details or backend implementation in Skills or
widget assets.
## License
The repository is public source, but the plugin and SHD branding are distributed
under the proprietary [SHD MCP Plugin License](LICENSE). Public visibility does
not grant rights to redistribute, resell, modify or reuse the package outside
the permitted SHD use.
@@ -0,0 +1,49 @@
{
"name": "shd-mcp-plugin",
"version": "0.5.1",
"description": "Ready-to-install SHD workflows backed by the authenticated SHD MCP server.",
"author": {
"name": "SHD",
"url": "https://shd.xyz.su"
},
"homepage": "https://shd.xyz.su",
"repository": "https://github.com/bulava92/shd-mcp-plugin",
"license": "Proprietary",
"keywords": [
"SHD",
"projects",
"ProjectBase",
"tasks",
"finance",
"CRM",
"documents",
"inventory",
"organizations",
"notifications",
"realtime",
"Gitea",
"MCP"
],
"skills": "./skills/",
"interface": {
"displayName": "SHD MCP",
"shortDescription": "SHD projects and operations",
"longDescription": "Installable SHD MCP workflows for projects, ProjectBase, tasks, files, finance, CRM, documents, access control, inventory, notifications, realtime, status monitoring and linked Gitea operations.",
"developerName": "SHD",
"category": "Productivity",
"capabilities": [
"Read",
"Write"
],
"websiteURL": "https://shd.xyz.su",
"brandColor": "#1E88E5",
"composerIcon": "./assets/logo.png",
"logo": "./assets/logo.png",
"defaultPrompt": [
"Покажи активные проекты с датой завершения и статусом.",
"Сделай сводку по проекту и его рискам.",
"Проверь ProjectBase без изменения данных."
]
},
"mcpServers": "./.mcp.json"
}
+9
View File
@@ -0,0 +1,9 @@
{
"mcpServers": {
"shd": {
"type": "http",
"url": "https://shd.xyz.su/mcp",
"oauth_resource": "https://shd.xyz.su/mcp"
}
}
}
+65
View File
@@ -0,0 +1,65 @@
# SHD MCP plugin package
This directory is the installable Codex plugin. It packages reusable SHD
workflow Skills and the official SHD MCP connection; it does not contain the
SHD backend or a second MCP server.
The plugin requires an SHD account authorized for the requested projects and
modules. Skills do not grant permissions or bypass server-side ACLs.
## Included workflows
- active projects with completion dates and statuses;
- project status and risk summaries;
- ProjectBase schema and data-quality audits;
- guarded task workflows;
- project file inspection and actions;
- finance and CRM read/audit workflows;
- finance account, balance, payment and settlement audits;
- analytics, comparisons, reports and task/proposal metrics;
- entity resolution and duplicate-candidate analysis;
- estimates, proposals and estimate-document-contract workflows;
- scheduling events, slots and booking positions;
- Wiki, notes, revisions and note-database workflows;
- discussions, channels, topics, messages and attachments;
- documents, templates, PDFs, revisions and public-link workflows;
- organizations, members, invitations, ACL and two-factor policies;
- notifications, preferences and read-state workflows;
- inventory, assets, stock, procurement and stocktake workflows;
- agent/controller diagnostics, tunnels, endpoints and runtime actions;
- status-page monitors, groups, history and incidents;
- realtime sessions, event cursors and operational activity;
- Terms documents, revisions, completeness and contract discussions;
- linked Gitea issues, labels, comments and attachments;
- safe bulk-change planning and validation rules.
When the connected MCP host supports MCP Apps UI, the 36 SHD render tools have
inline interactive widgets: active projects, module registers and specialized
Kanban, workload, timeline, funnel, dashboard, calendar, matrix, tree and
Project DB views. Each widget sorts or filters normalized records, shows
status/value fields, can load one selected records details where supported and
exposes only the write actions declared by its render metadata. A write always
requires an explicit form submission; destructive actions also require a
confirmation checkbox. After a successful mutation, the widget refreshes its
source list.
The same tools still return normal structured data for hosts that do not render
widgets.
The active-projects widget is versioned in `widgets/active-projects/v1/` with a
manifest, SHA256 checksum and source/provenance note. The SHD MCP backend reads
that artifact for the `ui://shd/active-projects/v1.html` resource.
The other versioned resources are stored under `widgets/` with a manifest,
SHA256 checksum and source/provenance note for each resource. Their render
tools are deliberately separate from the list tools so the server can keep
data access and UI rendering auditable.
## MCP connection
The package points to `https://shd.xyz.su/mcp` and requests OAuth for that MCP
resource. The client stores the resulting authorization; this repository never
contains an access token.
If a client or SHD installation uses another official endpoint, change the
connection in `.mcp.json` in that client's secure configuration. Do not commit
tokens, cookies, client secrets or private host credentials.
Binary file not shown.
@@ -0,0 +1,39 @@
---
name: shd-active-projects
description: Use when the user asks for all active SHD projects, project deadlines, project statuses, or a project register sorted by the nearest completion date.
---
# Active SHD projects
## Workflow
1. Call `shd_list_projects` once with `archived: false` and `limit: 100`.
2. If the user supplied a search term, pass it as `q`; otherwise do not add a
search filter.
3. Use the project fields returned by that tool. Do not call
`shd_get_project` once per row just to repeat list data.
4. Sort projects with a non-null `deadline` in ascending order. Put projects
without a deadline after dated projects; do not invent a date.
5. Return the compact table:
`Проект | Дата завершения | Статус`
6. If the user is in a UI-capable MCP host, call `shd_render_projects_widget`
after the data call, passing the returned `data` array as `projects` and the
returned `meta` object as `meta`. The widget is optional; the plain table
remains the fallback for hosts without MCP Apps UI.
7. Preserve the server's project name/code and status. If a deadline is a
datetime, format it with its returned timezone when available; otherwise
keep the value unmodified and state that timezone data was unavailable.
## Boundaries
- “Активные” defaults to non-archived projects (`archived: false`). If the user
means a particular business status rather than archive state, ask which
status or report the returned statuses without filtering them locally.
- Do not use task due dates as project completion dates.
- If the server indicates more records than the tool can return, report that
the current tool limit was reached; do not claim a complete list.
- This workflow is read-only and must not update, archive or delete projects.
See `references/project-widget.md` for the data/render boundary.
@@ -0,0 +1,5 @@
interface:
display_name: Active SHD projects
short_description: Project deadlines and statuses
policy:
allow_implicit_invocation: false
@@ -0,0 +1,5 @@
# Project widget contract
Call the data tool first. Pass only the returned project array and metadata to
the render tool. The widget is a presentation layer: it must not invent dates,
statuses or project rows and must keep the plain structured table as fallback.
@@ -0,0 +1,26 @@
---
name: shd-agents-operations
description: Use when the user asks to inspect SHD agents, controllers, diagnostics, tunnels, access rules, endpoints, monitoring or agent runtime actions.
---
# SHD agents operations
See `references/agent-operations.md` before touching runtime, network or access
configuration.
## Diagnose first
- Resolve the controller, user agent or tunnel identity from
`shd_agents_overview`, status and diagnostic tools.
- Read current access policies, settings, history and async status before
proposing an action.
- Keep diagnostics, configuration, endpoint publication and network routing as
separate operations. Never expose credentials, private keys or tunnel data.
## High-impact actions
Creating/closing tunnels, publishing or revoking endpoints, replacing access
policies, rebooting an agent, changing exit-network routing or enabling
monitoring requires an explicit target and user request. For asynchronous
actions, return the operation ID, poll only as needed and report terminal
status. Do not retry a network or reboot action blindly.
@@ -0,0 +1,5 @@
interface:
display_name: "SHD agents operations"
short_description: "Diagnose agents and guarded runtime actions"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,14 @@
# Agent operations contract
Start with `shd_agents_overview`, `shd_get_controller_status`,
`shd_agents_get_controller_diagnostics`, `shd_agents_get_diagnostics`, history
and the relevant access-policy tools. Use `shd_agents_get_async_status` for an
operation already returned by the server.
Treat tunnels, endpoint publication/revocation, access-policy replacement,
agent reboot, exit-node/network routing and controller/user-agent settings as
high-impact mutations. Tools such as `shd_agents_create_tunnel`,
`shd_agents_close_tunnel`, `shd_agents_publish_controller_endpoint`,
`shd_agents_revoke_controller_endpoint`, `shd_agents_reboot_user_agent` and
`shd_agents_force_exit_network_route` require explicit authorization and exact
scope. Preserve audit/readback and never print secrets returned by diagnostics.
@@ -0,0 +1,33 @@
---
name: shd-analytics
description: Use when the user asks for SHD project or company analytics, trends, comparisons, metrics, charts, reports, or time summaries.
---
# SHD analytics and reports
## Scope
1. Resolve the organization or project scope before aggregating values. Use an
exact `project_code` when the user supplied one; otherwise resolve it with
`shd_list_projects`.
2. Use `shd_get_project_context` for a bounded cross-module summary and request
only the sections needed for the question.
3. For task effort use `shd_get_task_time_report`; use
`shd_export_task_time_report` only when the user asks for an export.
4. For proposal analytics use `shd_get_proposal_public_analytics` only for a
resolved proposal or public analytics scope accepted by its schema.
5. If the requested metric does not map to a known tool, call
`shd_capabilities` and report the unavailable capability instead of inventing
an endpoint or field.
## Reporting rules
- State the period, scope, currency and timezone returned by SHD.
- Distinguish totals returned by the server from calculations made from rows.
- Preserve null, unavailable and permission-denied sections as separate states.
- Do not infer profitability, completion, approval or trend direction from one
record or from a missing section.
- Present chart-ready data as a compact table with labels, units and source
fields. Use a UI resource only when the server advertises a matching widget.
See `references/analytics-contract.md` before composing a multi-section report.
@@ -0,0 +1,5 @@
interface:
display_name: SHD analytics
short_description: Metrics, comparisons and reports
policy:
allow_implicit_invocation: false
@@ -0,0 +1,9 @@
# Analytics contract
- Every metric has an explicit scope, period, unit and source operation.
- A server total is authoritative; a locally calculated total must be marked as
calculated and list the rows used.
- A comparison must use the same period boundaries and unit on both sides.
- Missing data, denied access and a zero value are different outcomes.
- Recommendations follow the evidence and are not written back to SHD unless a
separate mutation is explicitly requested.
@@ -0,0 +1,26 @@
---
name: shd-discussions
description: Use when the user asks to find, read, search, create or edit SHD discussion channels, topics, messages or attachments.
---
# SHD discussions
See `references/discussion-contract.md` for the tool map and message safety
rules.
## Resolve and read
- Resolve the organization or project context before selecting a channel.
- Use `shd_list_discussion_channels` and `shd_list_discussion_topics` before
reading messages when an ID is not already known.
- Use bounded reads with `shd_list_discussion_messages` or
`shd_search_discussions`; preserve pagination and returned identifiers.
- Treat private channels and messages as server-authorized data. Do not infer
membership or quote content outside the returned scope.
## Mutations
Only create or edit a message, or upload an attachment, when the user asks for
that exact operation. Confirm the target topic, preserve the latest message
identity/version when available, and report the server result. Never delete or
rewrite discussion history by guessing a topic from its display name.
@@ -0,0 +1,5 @@
interface:
display_name: "SHD discussions"
short_description: "Search and manage discussion threads"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,14 @@
# Discussion contract
Use the narrowest tool for the request:
- discovery: `shd_list_discussion_channels`,
`shd_list_discussion_topics`;
- content: `shd_list_discussion_messages`, `shd_search_discussions`;
- writes: `shd_add_discussion_message`, `shd_update_discussion_message`;
- files: `shd_upload_discussion_attachment`.
Resolve the canonical channel/topic/message identifiers before a write. Keep
searches bounded, distinguish no results from denied scope, and retain the
server's pagination or conflict fields. A message edit or attachment upload is
a side effect: require explicit user intent and report the final server result.
@@ -0,0 +1,29 @@
---
name: shd-documents
description: Use when the user asks to inspect, generate, send, publish, revise or audit SHD documents, templates, comments or public links.
---
# SHD documents
See `references/document-safety.md` before handling publication, PDF or
contract-related document actions.
## Read and prepare
- Resolve the project and document identifiers before reading content.
- Read the document, relevant template/configuration, revisions and comments
needed for the requested result; do not treat a filename as authorization.
- Keep document status, approval state, external links and public links
separate in the report.
- Use the document's returned revision or version when a write accepts one.
- In a UI-capable MCP host, pass normalized list records to
`shd_render_documents_widget`; keep the structured document list as the
fallback and use `shd_get_contract` only for an explicitly selected record.
## Mutations and publication
Generating a PDF, sending a document, saving a contract, publishing a public
link or changing document state requires an explicit request. Explain the
target and audience before publication, preserve the server's ACL and expiry
rules, and perform readback when the tool provides it. Never expose private
links, bearer values or document content outside the authorized result.
@@ -0,0 +1,5 @@
interface:
display_name: "SHD documents"
short_description: "Inspect and safely manage documents"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,14 @@
# Document safety contract
Useful read tools include `shd_list_contracts`, `shd_get_contract`,
`shd_list_document_revisions`, `shd_list_document_templates`,
`shd_list_document_comments`, `shd_list_document_external_links` and
`shd_list_document_public_links`. PDF inspection uses
`shd_download_document_pdf`; generation uses `shd_generate_document_pdf`.
Treat these as separate side effects: `shd_send_document`,
`shd_publish_document_public_link`, `shd_save_document_contract` and
`shd_document_contract_workflow`. Before any of them, resolve the document,
read the current state and require the exact requested action. Report recipient,
visibility, expiry, revision and server validation when returned. A public link
is never a harmless preview and must not be created just to test access.
@@ -0,0 +1,26 @@
---
name: shd-entity-resolution
description: Use when the user asks to find a CRM entity, resolve duplicate names, compare counterparties, or determine whether records refer to the same business object.
---
# SHD entity resolution
## Resolve first
1. Prefer an exact returned ID, code or external reference over a name.
2. Search with `shd_list_crm`, then read candidates with
`shd_get_crm_entity`.
3. Inspect `shd_list_crm_entity_links` when relationships, projects or deals
are part of the identity decision.
4. Compare only fields returned by SHD. Report candidate records, matching
evidence, conflicting evidence and unresolved fields separately.
## Safety
- A similar name is not proof of a duplicate.
- Do not merge, delete, reassign ownership or change a relationship while
resolving identity.
- A write requires an exact target, an explicit user request and the current
server validation contract. Read the record again after a permitted write.
See `references/entity-resolution.md` for the candidate-report format.
@@ -0,0 +1,5 @@
interface:
display_name: SHD entity resolution
short_description: Resolve CRM records and duplicates
policy:
allow_implicit_invocation: false
@@ -0,0 +1,8 @@
# Entity-resolution report
Report each candidate as:
`candidate → identifiers → matching fields → conflicting fields → related records → confidence → next action`
Confidence is an explanation of returned evidence, not a server permission or
an authorization to merge records.
@@ -0,0 +1,28 @@
---
name: shd-estimate-management
description: Use when the user asks to inspect, compare, validate, create or update SHD estimates, proposals, terms or document contracts.
---
# SHD estimates and proposal documents
## Read path
1. Resolve the project, proposal, terms document or contract before acting.
2. Use `shd_list_proposals` and `shd_get_proposal` for proposal registers and
details.
3. Use `shd_list_terms_documents`, `shd_get_terms_document`,
`shd_get_terms_document_completeness` and `shd_validate_terms_document` for
terms and completeness checks.
4. Inspect template capabilities before proposing a template-driven change.
5. Report line items, versions, completeness, approvals and publication state
only when those fields are returned by the server.
## Mutations
- Creating, replacing items, requesting approval, publishing, restoring or
deleting a proposal/document requires an exact explicit request.
- Read the latest version and use the tool's concurrency and idempotency fields
before a write.
- Never publish or request approval as a side effect of an audit or comparison.
See `references/estimate-safety.md` before a proposal or terms mutation.
@@ -0,0 +1,5 @@
interface:
display_name: SHD estimates and proposals
short_description: Estimate and document workflows
policy:
allow_implicit_invocation: false
@@ -0,0 +1,7 @@
# Estimate and proposal safety
- Draft, validated, approved and published are separate states.
- A calculated total is not an approval and a generated PDF is not a
publication.
- Compare revisions before restoring or replacing a document.
- Keep the project, counterparty and version identifiers in the final report.
@@ -0,0 +1,25 @@
---
name: shd-event-positions
description: Use when the user asks about SHD events, booking availability, scheduling slots, event types or booking positions.
---
# SHD events and scheduling
## Read path
1. Start with `shd_get_scheduling_overview` when the scope is broad.
2. Use `shd_list_scheduling_event_types`,
`shd_list_scheduling_availability`, `shd_list_scheduling_slots` and
`shd_list_scheduling_bookings` for the exact requested view.
3. Preserve the returned timezone, slot state, booking state and integration
status. Do not turn an available slot into a confirmed booking.
## Mutations
`shd_hold_scheduling_slot`, `shd_create_scheduling_booking`,
`shd_confirm_scheduling_booking`, `shd_reschedule_scheduling_booking` and
`shd_cancel_scheduling_booking` are explicit writes. Read the current slot or
booking first, pass the current concurrency/idempotency values accepted by the
schema and report the final server state.
See `references/scheduling-safety.md` for the state boundaries.
@@ -0,0 +1,5 @@
interface:
display_name: SHD events and scheduling
short_description: Slots, bookings and event positions
policy:
allow_implicit_invocation: false
@@ -0,0 +1,7 @@
# Scheduling state boundaries
- Availability is not a hold.
- A hold is not a booking.
- A booking is not confirmed until SHD returns confirmation.
- A cancellation or reschedule must identify the exact booking and preserve
the server's conflict response.
@@ -0,0 +1,41 @@
---
name: shd-finance-crm
description: Use when the user asks for finance or CRM information, document/payment checks, customer records, deals, relationships, or a project business overview in SHD.
---
# SHD Finance and CRM
For a high-level project overview, prefer `shd_get_project_context` and request
only the `finance` or `summary` sections needed. For a detailed request use
the module tools below.
## Finance
- Find documents with `shd_list_finance_documents`.
- Read one with `shd_get_finance_document`.
- Read payments with `shd_list_finance_payments` or
`shd_get_finance_payment`.
- Resolve allowed references with `shd_list_finance_refs`.
- In a UI-capable MCP host, pass normalized finance records to
`shd_render_finance_widget`; keep the structured list as the fallback.
When auditing, compare document type, project/counterparty links, line items,
currency, totals, due dates, payment state and returned validation. Never infer
approval, payment or publication from a document's title or amount.
## CRM
- Find entities with `shd_list_crm` and read one with `shd_get_crm_entity`.
- In a UI-capable MCP host, pass normalized CRM records to
`shd_render_crm_widget`; keep the structured list as the fallback.
- Resolve relationships with `shd_list_crm_entity_links` and inspect available
pipelines/stages before a deal change.
- Use `shd_save_crm_entity` only for an explicitly requested create/update after
duplicate and relationship checks.
Do not approve, publish, delete, merge, move a deal stage, create a payment or
change ownership unless the user explicitly requests that exact operation.
Report IDs, resolved links, changed fields and server validation separately from
recommendations.
See `references/finance-crm-audit.md` for the finance/CRM audit boundary.
@@ -0,0 +1,5 @@
interface:
display_name: SHD finance and CRM
short_description: Finance and customer workflows
policy:
allow_implicit_invocation: false
@@ -0,0 +1,6 @@
# Finance and CRM audit boundary
Finance documents, payments, allocations, CRM entities, links and stages are
different objects. Report their identifiers and returned state separately.
Use the dedicated `shd-financial-account-audit` workflow for reconciliation,
balances, settlements or audit-log questions.
@@ -0,0 +1,30 @@
---
name: shd-financial-account-audit
description: Use when the user asks to audit SHD finance accounts, balances, payments, documents, allocations, settlements or finance history.
---
# SHD financial audit
## Read path
1. Resolve project, legal entity, counterparty and period before reading.
2. Use `shd_list_finance_refs` for allowed categories, accounts and reference
values when the request depends on them.
3. Read documents and payments with `shd_list_finance_documents`,
`shd_get_finance_document`, `shd_list_finance_payments` and
`shd_get_finance_payment`.
4. Use `shd_get_finance_account_balance` and
`shd_list_finance_audit_log` when balances or history are requested.
5. Reconcile only records returned for the same scope and period. Mark missing
links, currency differences and permission gaps explicitly.
## Settlement safety
- Allocation, removal of allocation, payment creation, approval, deletion and
document replacement are writes; do not perform them during an audit.
- A document total is not proof of payment, and a payment is not proof of
allocation.
- For an explicitly requested write, read the latest payment/document and use
the server's idempotency and concurrency contract, then read back the result.
See `references/finance-audit.md` for the reconciliation format.
@@ -0,0 +1,5 @@
interface:
display_name: SHD financial audit
short_description: Balances, payments and settlements
policy:
allow_implicit_invocation: false
@@ -0,0 +1,8 @@
# Finance audit format
Report:
`scope → period → currency → source record → observed amount/state → linked record → discrepancy → priority`
Do not hide an unresolved counterparty, currency conversion, allocation or
permission boundary behind an aggregate number.
@@ -0,0 +1,24 @@
---
name: shd-gitea
description: Use when the user asks to inspect or manage SHD-linked Gitea issues, labels, comments or issue attachments.
---
# SHD Gitea
See `references/gitea-contract.md` for repository and issue safety.
## Read and resolve
- Resolve the canonical repository and issue identifiers returned by SHD before
acting.
- Read the issue, labels and current state before creating a comment or
changing labels.
- Keep Gitea state separate from SHD project/task state; do not claim that a
label or issue update changed the SHD record unless the server says so.
## Mutations
Creating an issue, commenting, changing labels or uploading an attachment
requires an explicit request. Report repository, issue number, changed labels,
comment/attachment result and server validation. Never put credentials or
private attachment URLs in the response or repository.
@@ -0,0 +1,5 @@
interface:
display_name: "SHD Gitea"
short_description: "Review and update linked Gitea issues"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,11 @@
# Gitea contract
Use `shd_list_gitea_issues` and `shd_list_gitea_labels` for discovery. Writes
are `shd_create_gitea_issue`, `shd_comment_gitea_issue`,
`shd_update_gitea_issue_labels` and `shd_upload_gitea_issue_attachment`.
Resolve the repository and issue from returned server data, preserve the
current issue state before a label update, and use an idempotency key when the
tool schema offers one. A comment or attachment is an external side effect;
require exact user intent and report readback. Do not assume Gitea permissions
from SHD project permissions alone.
@@ -0,0 +1,26 @@
---
name: shd-inventory
description: Use when the user asks to inspect or change SHD inventory items, assets, stock, procurement, reservations, receipts or stocktakes.
---
# SHD inventory
See `references/inventory-safety.md` before any stock, asset or procurement
mutation.
## Inspect
- Resolve the project, location, item, asset or procurement identity from
server results.
- Read current stock, lots, reservations, movements, receipts, maintenance,
assignments and audit data relevant to the question.
- Keep available, reserved, received, written-off and counted quantities
distinct. Do not calculate an authoritative balance from partial pages.
## Mutations
Reserve, release, receive, write off or close a stocktake only on an explicit
request. Creating/updating resources and generic inventory actions require the
same read-before-write and conflict/idempotency handling. Report quantities,
location, source document, server validation and readback; never silently
repair a discrepancy by changing stock.
@@ -0,0 +1,5 @@
interface:
display_name: "SHD inventory"
short_description: "Audit stock, assets and procurement"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,14 @@
# Inventory safety contract
Read tools cover items/assets, procurement, locations, movements, lots,
reservations, receipts, stocktakes, labels, assignments, maintenance and the
inventory audit log. Use the narrowest `shd_list_inventory_*` or
`shd_get_inventory_*` tool and preserve pagination.
Side-effecting operations include `shd_reserve_inventory_stock`,
`shd_release_inventory_stock`, `shd_receive_inventory_stock`,
`shd_write_off_inventory_stock`, `shd_close_inventory_stocktake`,
`shd_create_inventory_resource`, `shd_update_inventory_resource` and
`shd_inventory_action`. Resolve the exact target and latest quantity first;
require explicit confirmation for irreversible or bulk operations and verify
the resulting movement/audit record.
@@ -0,0 +1,25 @@
---
name: shd-notifications
description: Use when the user asks to inspect SHD notifications, notification modules, read state or preferences, or mark notifications read.
---
# SHD notifications
See `references/notification-contract.md` for read-state and preference
boundaries.
## Read
- Use `shd_list_notifications` with bounded filters and preserve notification
IDs and timestamps.
- Use module, read-state and preference tools only for the authenticated user
or the explicitly authorized scope.
- Distinguish unread, read, muted and unavailable data; do not invent a missing
notification source.
## State changes
Marking one or more notifications read or changing preferences changes user
state. Do it only when explicitly requested, use the returned IDs, and report
the number and final state. Never mark a whole feed read merely because it was
displayed or summarized.
@@ -0,0 +1,5 @@
interface:
display_name: "SHD notifications"
short_description: "Review alerts and notification state"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,11 @@
# Notification contract
Read with `shd_list_notifications`, `shd_list_notification_modules`,
`shd_get_notification_read_state`, `shd_get_notification_preferences` and,
for document-specific items, `shd_list_document_notifications`.
State-changing tools are `shd_update_notification_preferences`,
`shd_mark_notification_read`, `shd_mark_all_notifications_read` and
`shd_mark_notifications_read`. Require explicit scope for bulk marking,
prefer stable notification IDs, and report server readback. A list response is
not authorization to alter every item in it.
@@ -0,0 +1,26 @@
---
name: shd-organizations-acl
description: Use when the user asks to inspect or change SHD organizations, members, invitations, roles, access audits or two-factor policies.
---
# SHD organizations and ACL
See `references/organization-acl.md` for identity resolution and mutation
boundaries.
## Inspect
- Resolve the canonical organization ID; never use a guessed name as a write
target.
- Read members, invitations, audit entries and the current two-factor policy
before explaining access.
- Report organization, user, role, invitation state and policy as separate
facts. A denied or incomplete list is not proof that a member is absent.
## Change access
Inviting, adding, updating or removing a member, changing a two-factor policy,
or creating, archiving or restoring an organization requires explicit intent.
Show the exact organization and user scope before the mutation. Preserve server
authorization, conflict checks and audit readback; never broaden access to
make a failed request succeed.
@@ -0,0 +1,5 @@
interface:
display_name: "SHD organizations and ACL"
short_description: "Audit members, roles and access policies"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,13 @@
# Organization and ACL contract
Read with `shd_list_organization_members`, `shd_list_organization_invitations`,
`shd_list_organization_audit` and
`shd_get_organization_two_factor_policy`. Resolve users and organizations from
server-returned IDs.
Mutations include invitation operations, member add/update/remove,
organization create/update/archive/restore and
`shd_update_organization_two_factor_policy`. Every mutation needs explicit
authorization, the latest relevant state, and final server/audit readback when
available. Do not disclose invitation secrets or use an email/name match as a
substitute for a returned user identity.
@@ -0,0 +1,39 @@
---
name: shd-project-db-audit
description: Use when the user asks to inspect ProjectBase tables, validate project data, find duplicate or broken links, or audit ProjectBase quality without changing records.
---
# ProjectBase audit
See `references/audit-contract.md` for the bounded read and evidence format.
## Workflow
1. Resolve the canonical Base identity. A supplied `project_code` is accepted
as the legacy alias only when the server accepts it; prefer the returned
Base identity for subsequent calls.
2. If no table is named, call `shd_project_db_tables` first and select only the
relevant tables. Do not load every table by default.
3. Call `shd_project_db_schema` for each selected table before reading its
records. Pass the table explicitly when a single-table audit is requested.
4. Call `shd_project_db_records` with bounded reads and the selected table.
5. Compare records with the returned schema and report only evidence visible in
the response.
## Findings
Check for duplicate business numbers, missing required values, invalid select
values, unresolved linked records, orphaned references, conflicting DALI or
electrical identifiers, formula/derived-field risks and incomplete source data
when the selected tables expose those fields.
Report `table → record → field → observed value → reason → priority`. Distinguish
confirmed defects from data that could not be checked because the schema or
records were unavailable.
## Safety
This workflow is read-only. Do not call create, update, delete, import, bulk
upsert or generic action tools while auditing. If the user asks to repair
findings, first present the proposed records and then use the dedicated
validation/dry-run contract before any explicit write.
@@ -0,0 +1,5 @@
interface:
display_name: ProjectBase audit
short_description: Read-only schema and data audit
policy:
allow_implicit_invocation: false
@@ -0,0 +1,20 @@
# ProjectBase audit contract
Use this contract for every read-only ProjectBase audit.
1. Resolve the canonical Base identity before selecting tables. Treat a
user-provided project code as an alias until the server returns the
authoritative identity.
2. Discover tables only when the request does not name them. Do not load every
table just to find a possible issue.
3. Read the schema before records for every selected table. Keep the table
name explicit in each subsequent request.
4. Bound record reads with the smallest useful limit and preserve the server's
pagination or truncation metadata.
5. Report evidence as `table → record → field → observed value → reason →
priority`. Mark a finding as unverified when the schema or record needed to
prove it was unavailable.
Never repair an audit finding inside the audit workflow. A repair requires a
separate proposal, an explicit mutation request, the current schema and
records, the server's validation/dry-run contract, and final readback.
@@ -0,0 +1,30 @@
---
name: shd-project-files
description: Use when the user asks to inspect project files, obtain a file link, share or zip a file, rename or move a path, or delete project files.
---
# SHD project files
## Resolve paths
1. Call `shd_list_files` with the exact `project_code` and, when supplied, the
path.
2. Resolve the exact file or directory path and distinguish files from
directories before any action.
3. Use `shd_get_file_link` only when the user asks for a temporary file link or
download.
## Actions
`shd_create_file_action` supports the explicit actions `share-link`, `zip-link`,
`rename`, `move` and `delete`. Use the exact action matching the request. For
rename, move and delete, confirm the source/destination and replacement scope
from the current listing first. Mutating actions require a stable
`idempotency_key`; delete additionally requires `confirm: true` and the server
version/confirmation fields accepted by its schema.
Do not expose bearer tokens or raw secret material in prose or logs. Return a
temporary URL only when the user explicitly asks for it. Do not overwrite,
publish, share or delete a path merely because the user asked to inspect it.
See `references/file-actions.md` for the file-action boundary.
@@ -0,0 +1,5 @@
interface:
display_name: SHD project files
short_description: Inspect and safely manage files
policy:
allow_implicit_invocation: false
@@ -0,0 +1,5 @@
# File action boundary
Resolve the exact project path first. Inspection, temporary link retrieval,
share/zip, rename/move and delete are separate intents. Mutating actions need
the server's current version, confirmation and idempotency fields.
@@ -0,0 +1,41 @@
---
name: shd-project-status
description: Use when the user asks for a status report, operational summary, risks, blockers, next actions, or a cross-module overview of one SHD project.
---
# SHD project status
## Resolve the project
- If the user supplied an exact project code, use it directly.
- If the user supplied only a name, call `shd_list_projects` with `q` once,
choose only an unambiguous exact match, and ask for the project code when
multiple projects match.
## Read the context
For a full summary call `shd_get_project_context` with the resolved
`project_code`, `include` set to the requested sections, and
`limit_per_section: 100`. The available sections are:
`summary`, `tasks`, `discussions`, `documents`, `finance`, `terms`, `agents`.
Request only the sections needed for a narrow question. Do not pretend that a
section is empty when the server reported it unavailable or the user lacks
access.
## Report
Separate the result into:
- confirmed project state;
- current deadlines or status values returned by SHD;
- blockers and risks supported by retrieved records;
- next actions, clearly marked as recommendations;
- unavailable or unresolved sections.
Do not convert a task deadline into a project deadline, infer a completion date
from activity, or claim deployment success from a status field. This workflow is
read-only unless the user separately requests a specific change.
See `references/context-sections.md` for section selection and unavailable data.
@@ -0,0 +1,5 @@
interface:
display_name: SHD project status
short_description: Project summaries and risks
policy:
allow_implicit_invocation: false
@@ -0,0 +1,5 @@
# Project context sections
Use only the sections requested by the user. Available sections currently
include `summary`, `tasks`, `discussions`, `documents`, `finance`, `terms` and
`agents`; unavailable or denied sections must remain visible in the report.
@@ -0,0 +1,23 @@
---
name: shd-realtime-activity
description: Use when the user asks to inspect SHD realtime events, start or terminate a realtime session, or review operational activity and audit history.
---
# SHD realtime and activity
See `references/realtime-contract.md` for session lifecycle and event handling.
## Read and observe
- Resolve the authorized project/module scope before reading events or audit
history.
- Use bounded event reads and preserve event IDs, cursors and timestamps.
- Distinguish historical audit records from a live realtime session; do not
claim that an event stream is complete when it was truncated or disconnected.
## Session lifecycle
Starting or terminating a realtime session is a stateful operation. Require an
explicit request, retain the returned session ID and scope, avoid duplicate
starts on retry, and terminate only the requested session. Report connection
or terminal state rather than fabricating an event.
@@ -0,0 +1,5 @@
interface:
display_name: "SHD realtime and activity"
short_description: "Inspect events and manage live sessions"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,11 @@
# Realtime and activity contract
Use `shd_start_realtime_session`, `shd_get_realtime_events` and
`shd_terminate_realtime_session` for the session lifecycle. Use the relevant
read-only audit/activity tool for historical records, such as
`shd_get_audit_history`, when the request is about what already happened.
Keep session IDs, scopes, cursors and last-seen timestamps separate. A retry
must not create a second session unless the server explicitly supports an
idempotency key. Termination requires the exact returned session ID and should
be confirmed by terminal readback.
@@ -0,0 +1,65 @@
---
name: shd-routing
description: Use for any request that asks SHD for data, a project operation, a module workflow, or a change to SHD records. Route the request to the narrowest existing SHD MCP tool and preserve server-side permissions.
---
# SHD routing and safety
SHD MCP is the source of truth for live data, tool schemas, permissions,
organization/project scope, conflicts, idempotency, audit and persisted
readback. Do not invent fields, statuses, identifiers, routes or capabilities.
## Route the request
- Project list, project status, deadlines or project context: use the Projects
Core tools and the `shd-active-projects` or `shd-project-status` workflow.
- ProjectBase schema, records, links or data quality: use Project DB tools and
the `shd-project-db-audit` workflow.
- Tasks, task projects, task status or task dates: use Tasks tools and the
`shd-task-workflow` workflow.
- Project files and paths: use Files tools and the `shd-project-files` workflow.
- Discussions, channels, topics, messages or attachments: use Discussions tools and the `shd-discussions` workflow.
- Documents, templates, revisions, PDFs or publication links: use Documents tools and the `shd-documents` workflow.
- Organizations, members, invitations, roles or two-factor policy: use Organizations tools and the `shd-organizations-acl` workflow.
- Notifications, preferences or read state: use Notifications tools and the `shd-notifications` workflow.
- Inventory, stock, assets, procurement or stocktakes: use Inventory tools and the `shd-inventory` workflow.
- Agents, controllers, diagnostics, tunnels or network access: use Agents tools and the `shd-agents-operations` workflow.
- Status-page monitors, history, groups or incidents: use Status Page tools and the `shd-status-page` workflow.
- Realtime sessions, event cursors or historical activity: use Realtime tools and the `shd-realtime-activity` workflow.
- Terms documents, revisions, completeness or contract discussions: use Terms tools and the `shd-terms-contracts` workflow.
- Linked Gitea issues, labels, comments or attachments: use Gitea tools and the `shd-gitea` workflow.
- Finance or CRM records: use the specific module tools and the
`shd-finance-crm` workflow; use project context for a high-level overview.
- Finance reconciliation, balances, settlements or audit history:
`shd-financial-account-audit`.
- Proposals, estimates, quotes or estimate-document contracts:
`shd-estimate-management`.
- Scheduling, event types, slots or bookings: `shd-event-positions`.
- Wiki, notes, pages, revisions or note databases: `shd-wiki-management`.
- Entity matching or possible duplicates: `shd-entity-resolution`.
- Analytics, comparisons, trends or reports: `shd-analytics`.
- A multi-record change: use `shd-safe-bulk-change` before any mutation.
Use `shd_capabilities` only when the available module or permission boundary is
unclear. If a tool or module is unavailable, report that fact instead of
falling back to an undocumented endpoint.
## Read-first rule
For every write, first read the target and its relevant schema or metadata.
Only perform a mutation when the user explicitly requested that mutation. A
request to inspect, summarize, compare, audit or suggest is not permission to
write.
Preserve the exact server result. Separate confirmed values from inference,
and include affected identifiers, conflicts, validation errors and final
readback when the server returns them.
## Scope and secrets
Never ask for or expose access tokens, passwords, cookies, private keys, raw
public-link bearer tokens or integration secrets. Never use a user-provided
project name as an identifier when SHD has not resolved it to a returned
project code or ID.
See `references/module-map.md` for the current high-level routing map.
@@ -0,0 +1,5 @@
interface:
display_name: SHD routing and safety
short_description: Route requests to safe MCP tools
policy:
allow_implicit_invocation: false
@@ -0,0 +1,25 @@
# SHD module map
- Projects and project context → `shd_list_projects`, `shd_get_project`,
`shd_get_project_context`.
- ProjectBase → `shd_project_db_*`, `shd_project_base_*`.
- Tasks → `shd_list_tasks`, `shd_get_task`, task mutation tools.
- Files → `shd_list_files`, file action tools.
- Discussions → channel, topic, message search and attachment tools.
- Documents → document, template, revision, PDF and publication-link tools.
- Organizations and ACL → members, invitations, audit and two-factor policy
tools.
- Notifications → notification, preference and read-state tools.
- Inventory → stock, asset, procurement, reservation and stocktake tools.
- Agents → controller/user-agent status, diagnostics, tunnels, endpoints and
network-operation tools.
- Status Page → monitor, history, group and incident tools.
- Realtime and activity → session, event-cursor and audit/activity tools.
- Terms and contracts → Terms documents, completeness, revisions and point
discussion tools.
- Gitea → linked issue, label, comment and attachment tools.
- Finance and CRM → module-specific finance/CRM tools.
- Proposals, terms and document contracts → proposal/terms/document tools.
- Scheduling → `shd_get_scheduling_overview` and scheduling tools.
- Wiki and notes → note-space, page, revision and database tools.
- Unknown or permission-sensitive capability → `shd_capabilities` first.
@@ -0,0 +1,30 @@
---
name: shd-safe-bulk-change
description: Use before any SHD bulk create, update, import, archive, delete, or other multi-record mutation, and whenever the user asks to plan a mass change.
---
# Safe SHD bulk changes
## Plan first
1. Confirm the target module, organization/project scope, exact match rule and
intended side effect.
2. Read current schemas and records. Estimate affected, skipped, invalid and
ambiguous rows.
3. Prefer a server validation or dry-run operation. For ProjectBase use
`shd_project_db_validate_records`, then
`shd_project_db_bulk_create_records` or
`shd_project_db_bulk_upsert_records` with `dry_run: true` when applicable.
4. Present the plan and stop unless the user explicitly authorized execution.
## Execute safely
Use stable idempotency keys, expected versions and exact match fields accepted
by the selected tool. Do not silently widen a project, organization, table or
record filter. Do not convert a failed or conflicting row into a new record.
After execution, report created, updated, skipped, rejected and conflicted
rows, then use the server's persisted readback when available. Destructive
operations require explicit confirmation and remain subject to server ACL.
See `references/write-preflight.md` for the required preflight record.
@@ -0,0 +1,5 @@
interface:
display_name: SHD safe bulk changes
short_description: Plan and validate bulk mutations
policy:
allow_implicit_invocation: false
@@ -0,0 +1,7 @@
# Write preflight
Before a multi-record write, record:
`module → scope → match rule → current count → affected count → skipped/ambiguous count → validation result → idempotency key`
Stop on an ambiguous match, validation failure, conflict or missing readback.
@@ -0,0 +1,22 @@
---
name: shd-status-page
description: Use when the user asks to inspect SHD status-page monitors, history, groups or incidents, or to change public status monitoring.
---
# SHD status page
See `references/status-page-safety.md` for monitor and incident boundaries.
## Read
- Resolve the status-page scope and monitor IDs before reporting health.
- Read current monitors and history; distinguish current state from historical
observations and from an incident declaration.
- Report unavailable, stale or unauthorized monitors explicitly.
## Mutations
Publishing an incident, creating/updating/deleting a group or monitor changes
an externally visible monitoring surface. Require an explicit request, exact
scope and current readback. Do not publish an incident to test the integration
and do not infer an outage from one missing response.
@@ -0,0 +1,5 @@
interface:
display_name: "SHD status page"
short_description: "Inspect monitors and publish incidents safely"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,14 @@
# Status-page safety contract
Read with `shd_get_status_page_monitors` and
`shd_get_status_page_monitor_history`. Mutations are
`shd_publish_status_page_incident`, `shd_create_status_page_group`,
`shd_update_status_page_group`, `shd_delete_status_page_group`,
`shd_create_status_page_monitor`, `shd_update_status_page_monitor` and
`shd_delete_status_page_monitor`.
Before a mutation, resolve the canonical monitor/group IDs, read the latest
state and show the public scope, message, duration or target being changed.
Report server validation and final status. Deletion and incident publication
are consequential actions and must never be inferred from a question asking for
diagnosis.
@@ -0,0 +1,44 @@
---
name: shd-task-workflow
description: Use when the user asks to find, inspect, create, update, reschedule, archive, or delete SHD tasks or task-project data.
---
# SHD task workflow
See `references/task-contract.md` for identifier resolution, conflict handling
and mutation readback requirements.
## Read and resolve
- Use `shd_list_task_projects` when the request concerns a task project or its
status.
- Use `shd_list_tasks` with `project_code`, `project_id`, `q`, `archived`,
`limit` or `offset` to find tasks.
- Use `shd_get_task` for one resolved task and
`shd_get_task_project_meta` for statuses, tags, sprints, views and custom
fields.
- Read comments or attachments only when they are relevant to the request.
- After `shd_list_tasks` returns, always pass normalized task records to
`shd_render_tasks_widget` before the final response when the host supports
MCP Apps UI. Use the structured task list or plain text only when the render
tool is unavailable or returns an error.
Do not create a task from an ambiguous project name or duplicate a task that
already matches the request.
## Mutations
Only mutate when the user explicitly asks for the exact operation:
- create with `shd_create_task`;
- update fields with `shd_update_task`;
- change status with `shd_update_task_status`;
- archive with `shd_archive_task`;
- delete with `shd_delete_task` and the server's explicit confirmation contract.
Before updating, pass the latest `updated_at` when the tool accepts it. Use an
idempotency key when the tool schema accepts one. If the server reports a
conflict, stop and show the current state; do not overwrite it by guessing.
Report the task ID, changed fields, server validation, conflict state and final
status. Never treat a suggested change as authorization to execute it.
@@ -0,0 +1,5 @@
interface:
display_name: SHD task workflow
short_description: Read and safely update tasks
policy:
allow_implicit_invocation: false
@@ -0,0 +1,20 @@
# Task workflow contract
Resolve task context before acting:
- use a canonical task-project identifier when the server returns one;
- search by explicit project, task ID or bounded query rather than guessing
from a display name;
- read task-project metadata before changing status, sprint, tags or custom
fields;
- retain `updated_at`, version or equivalent conflict data from the latest
read whenever the mutation schema supports it.
For a write, report the exact operation, task ID, changed fields, idempotency
key when used, server validation, conflict result and readback state. A
conflict or ambiguous match is a stop condition: show the current state and
ask for a narrower target instead of overwriting it.
Delete and archive operations require their server-side confirmation contract.
Never treat a proposed task change or a natural-language suggestion as write
authorization.
@@ -0,0 +1,24 @@
---
name: shd-terms-contracts
description: Use when the user asks to inspect, compare, discuss, revise or restore SHD Terms documents and contract points.
---
# SHD Terms and contracts
See `references/terms-contract.md` for revision, completeness and discussion
rules.
## Inspect
- Resolve the canonical Terms document and project context.
- Read completeness, current revision and relevant history before comparing or
summarizing contractual content.
- Keep document text, comments, point discussions and completeness findings
distinct; identify unavailable sections instead of filling them in.
## Changes
Restoring a revision or adding a contract comment/discussion point requires an
explicit request and the exact document/point scope. Preserve the latest
revision or conflict token where available, report the created comment or
restored revision, and never present a draft as an approved contract.
@@ -0,0 +1,5 @@
interface:
display_name: "SHD Terms and contracts"
short_description: "Review revisions and contract completeness"
policy:
allow_implicit_invocation: false
@@ -0,0 +1,11 @@
# Terms and contract contract
Read with `shd_list_terms_documents`, `shd_get_terms_document`,
`shd_get_terms_document_completeness`, `shd_list_terms_document_revisions` and
`shd_get_terms_document_revision`. Use `shd_terms_description_comment` and
`shd_terms_point_discussion` only for explicit comment/discussion requests.
`shd_restore_terms_document_revision` is a mutation: resolve the document and
revision, read current state, require confirmation and report readback. Never
restore merely because an older revision looks cleaner, and never confuse
completeness analysis with legal approval.
@@ -0,0 +1,26 @@
---
name: shd-wiki-management
description: Use when the user asks to search, read, compare, create or update SHD Wiki, notes, pages, collections, databases, revisions or attachments.
---
# SHD Wiki and notes
## Read path
1. Resolve the note space and page tree with `shd_list_note_spaces`,
`shd_get_note_space_tree` and `shd_list_note_pages`.
2. Search with `shd_search_note_blocks`, then read the selected page with
`shd_read_note_document` or `shd_get_note_page`.
3. Use revision and operation tools to compare versions before proposing a
replacement or restore.
4. Keep attachments, backlinks, comments, database records and page content as
separate objects in the report.
## Mutations
Creating, replacing, moving, archiving, deleting, importing, granting members
or changing review state requires an exact explicit request. Read the current
object first, preserve the latest revision/version and use the dedicated
operation contract. Read back the persisted result after a successful write.
See `references/wiki-safety.md` for content and revision boundaries.
@@ -0,0 +1,5 @@
interface:
display_name: SHD Wiki and notes
short_description: Pages, revisions and note databases
policy:
allow_implicit_invocation: false
@@ -0,0 +1,7 @@
# Wiki safety
- A search result is not the full page.
- A revision diff is not a write proposal.
- A replacement must preserve the selected page and revision identifiers.
- Deleting a page, collection, database or attachment requires an exact target
and the server's explicit confirmation fields.
@@ -0,0 +1,20 @@
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
.shd-widget-actions[hidden]{display:none}
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
.shd-widget-actions-field:has(textarea){grid-column:span 2}
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
.shd-widget-actions-danger{color:var(--danger,#b42318)}
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
@@ -0,0 +1,429 @@
(function () {
'use strict';
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
var copy = {
ru: {
actions: 'Действия',
chooseAction: 'Выберите действие',
selectRecord: 'Сначала выберите запись.',
submit: 'Сохранить',
cancel: 'Отмена',
confirm: 'Подтверждаю действие',
warning: 'Проверьте данные перед записью.',
destructive: 'Операция может изменить или удалить данные.',
required: 'Заполните обязательное поле.',
invalidJson: 'Введите корректный JSON.',
success: 'Изменения сохранены.',
failure: 'Не удалось сохранить изменения.',
loading: 'Сохраняю…',
noActions: 'Для этого виджета нет доступных действий.',
refreshed: 'Список обновлён.',
fieldValue: 'Значение',
},
en: {
actions: 'Actions',
chooseAction: 'Choose an action',
selectRecord: 'Select a record first.',
submit: 'Save',
cancel: 'Cancel',
confirm: 'I confirm this action',
warning: 'Review the values before writing.',
destructive: 'This operation may change or remove data.',
required: 'Fill in the required field.',
invalidJson: 'Enter valid JSON.',
success: 'Changes saved.',
failure: 'Could not save changes.',
loading: 'Saving…',
noActions: 'No actions are available for this widget.',
refreshed: 'List refreshed.',
fieldValue: 'Value',
},
};
var state = {
widget: {},
records: [],
selected: null,
action: null,
inputs: {},
busy: false,
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
};
var pending = new Map();
var nextRequestId = 1;
var elements = {};
function t(key) {
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
}
function object(value) {
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}
function normalize(value) {
if (!value) return null;
if (value.structuredContent) return normalize(value.structuredContent);
if (value.toolOutput) return normalize(value.toolOutput);
if (value.toolInput) return normalize(value.toolInput);
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
return value;
}
function widgetFrom(value) {
var payload = normalize(value) || {};
var meta = object(payload.meta) ? payload.meta : {};
return object(meta.widget) ? meta.widget : {};
}
function recordsFrom(value) {
var payload = normalize(value) || {};
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
if (!Array.isArray(data)) data = payload.data;
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
}
function readPath(value, path) {
if (!value || path === undefined || path === null) return undefined;
var parts = String(path).split('.');
var current = value;
for (var index = 0; index < parts.length; index += 1) {
if (current === undefined || current === null) return undefined;
current = current[parts[index]];
}
return current;
}
function firstValue(paths) {
var list = Array.isArray(paths) ? paths : [paths];
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
var sourceObject = sources[sourceIndex];
if (!sourceObject) continue;
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
var value = readPath(sourceObject, list[pathIndex]);
if (value !== undefined && value !== null && value !== '') return value;
}
}
return undefined;
}
function text(value) {
if (value === undefined || value === null) return '';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
}
function label(value) {
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
return String(value || t('fieldValue'));
}
function actionLabel(action) {
return label(action && action.label) || action.id || action.tool || t('actions');
}
function hasTargets(action) {
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
}
function actionList() {
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
return action && action.tool;
}) : [];
}
function request(method, params) {
var openai = typeof window !== 'undefined' ? window.openai : undefined;
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
return openai.callTool(params.name, params.arguments || {});
}
var id = nextRequestId++;
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
return new Promise(function (resolve, reject) {
var timeout = window.setTimeout(function () {
pending.delete(id);
reject(new Error(t('failure')));
}, 15000);
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
});
}
function unwrap(value) {
var payload = normalize(value);
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
if (payload && payload.error) throw new Error(text(payload.error));
return payload || value;
}
function make(tag, value, className) {
var node = document.createElement(tag);
if (className) node.className = className;
if (value !== undefined) node.textContent = value;
return node;
}
function ensurePanel() {
if (elements.panel) return;
var root = document.querySelector('main') || document.body;
var panel = make('section', undefined, 'shd-widget-actions');
panel.hidden = true;
panel.setAttribute('aria-live', 'polite');
var header = make('div', undefined, 'shd-widget-actions-header');
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
var select = document.createElement('select');
select.className = 'shd-widget-actions-select';
select.setAttribute('aria-label', t('actions'));
var form = make('form', undefined, 'shd-widget-actions-form');
var status = make('div', '', 'shd-widget-actions-status');
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
submit.type = 'submit';
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
cancel.type = 'button';
buttons.append(submit, cancel);
panel.append(header, select, form, status, buttons);
root.append(panel);
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
select.addEventListener('change', function () {
var selected = actionList().find(function (action) { return action.id === select.value; });
state.action = selected || null;
renderForm();
});
cancel.addEventListener('click', function () {
state.action = null;
select.value = '';
renderForm();
});
form.addEventListener('submit', function (event) {
event.preventDefault();
submitAction();
});
}
function inputValue(fieldConfig) {
var value = firstValue(fieldConfig.source || []);
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
return value;
}
function createInput(fieldConfig) {
var type = fieldConfig.type || 'text';
var input;
if (type === 'textarea' || type === 'json') {
input = document.createElement('textarea');
input.rows = type === 'json' ? 4 : 3;
if (type === 'json') input.placeholder = '{ }';
} else if (type === 'select') {
input = document.createElement('select');
(fieldConfig.options || []).forEach(function (option) {
var item = document.createElement('option');
item.value = String(option[0]);
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
input.append(item);
});
} else {
input = document.createElement('input');
input.type = type === 'number' || type === 'email' ? type : 'text';
}
input.className = 'shd-widget-actions-input';
input.name = fieldConfig.name;
var value = inputValue(fieldConfig);
if (type === 'checkbox') {
input.type = 'checkbox';
input.checked = value === true || value === 'true' || value === 1 || value === '1';
} else if (value !== undefined && value !== null) {
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
}
if (fieldConfig.required) input.required = true;
return input;
}
function renderForm() {
ensurePanel();
var selectedActionId = state.action ? state.action.id : '';
elements.select.replaceChildren();
var placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = t('chooseAction');
elements.select.append(placeholder);
actionList().forEach(function (action) {
var option = document.createElement('option');
option.value = action.id;
option.textContent = actionLabel(action);
elements.select.append(option);
});
elements.select.value = selectedActionId;
elements.select.hidden = actionList().length === 0;
elements.form.replaceChildren();
elements.status.textContent = '';
elements.submit.disabled = state.busy;
var action = state.action;
if (!action) {
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
elements.submit.hidden = true;
elements.cancel.hidden = true;
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
}
return;
}
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
elements.submit.hidden = false;
elements.cancel.hidden = false;
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
(action.fields || []).forEach(function (fieldConfig) {
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
var wrapper = make('label', undefined, 'shd-widget-actions-field');
wrapper.append(make('span', label(fieldConfig.label)));
wrapper.append(createInput(fieldConfig));
elements.form.append(wrapper);
});
if (action.confirm) {
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
var confirm = document.createElement('input');
confirm.type = 'checkbox';
confirm.name = '__confirm';
confirm.required = true;
confirmLabel.append(confirm, make('span', t('confirm')));
elements.form.append(confirmLabel);
}
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
elements.form.append(notice);
}
function coerce(value, type) {
if (type === 'number') {
var number = Number(value);
return Number.isFinite(number) ? number : value;
}
if (type === 'checkbox') return Boolean(value);
if (type === 'json') {
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
}
return value;
}
function targetType(name) {
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
}
function targetList(action) {
if (Array.isArray(action.targets)) return action.targets;
return action.target ? [action.target] : [];
}
function collectArguments(action) {
var args = Object.assign({}, action.fixed || {});
(action.context || []).forEach(function (name) {
var value = firstValue([name]);
if (value !== undefined && value !== null && value !== '') args[name] = value;
});
targetList(action).forEach(function (targetConfig) {
var value = firstValue(targetConfig.source || targetConfig.name);
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
});
(action.fields || []).forEach(function (fieldConfig) {
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
return node.getAttribute('name') === fieldConfig.name;
});
if (!input) return;
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
if (raw === '' && !input.checked && !fieldConfig.required) return;
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
});
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
if (action.confirmArg) args.confirm = true;
return args;
}
function setStatus(message, error) {
elements.status.textContent = message || '';
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
}
function submitAction() {
if (state.busy || !state.action) return;
var action = state.action;
var args;
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
state.busy = true;
elements.submit.disabled = true;
elements.cancel.disabled = true;
setStatus(t('loading'), false);
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
unwrap(result);
setStatus(t('success'), false);
var refresh = document.getElementById('refresh');
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
}).catch(function (error) {
setStatus(error && error.message ? error.message : t('failure'), true);
}).finally(function () {
state.busy = false;
elements.submit.disabled = false;
elements.cancel.disabled = false;
});
}
function recordFromNode(node) {
var content = String(node.textContent || '').toLowerCase();
var exact = state.records.find(function (item) {
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
return id && content.indexOf(id) !== -1;
});
if (exact) return exact;
return state.records.find(function (item) {
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
return title && content.indexOf(title) !== -1;
}) || null;
}
function update(value) {
var payload = normalize(value) || {};
var widget = widgetFrom(payload);
if (Object.keys(widget).length) state.widget = widget;
var records = recordsFrom(payload);
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
ensurePanel();
var actions = actionList();
elements.panel.hidden = actions.length === 0;
if (actions.length && !state.action) renderForm();
else if (actions.length) renderForm();
}
document.addEventListener('click', function (event) {
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
if (!node || (elements.panel && elements.panel.contains(node))) return;
var item = recordFromNode(node);
if (!item) return;
state.selected = item;
ensurePanel();
renderForm();
}, true);
window.addEventListener('message', function (event) {
if (event.source !== window.parent) return;
var message = event.data;
if (!message || message.jsonrpc !== '2.0') return;
if (message.id !== undefined && pending.has(message.id)) {
var requestState = pending.get(message.id);
pending.delete(message.id);
window.clearTimeout(requestState.timeout);
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
else requestState.resolve(message.result);
return;
}
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
}, { passive: true });
ensurePanel();
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
if (initial) update(initial);
}());
@@ -0,0 +1 @@
818956bcece3e1a64a8f41f51a8bcc19ebcd0c95d1eb9bfa923166dca9660711 index.html
@@ -0,0 +1,5 @@
# ACL matrix widget
This artifact is served by the SHD MCP backend as ui://shd/acl-matrix/v1.html.
The HTML is standalone, uses the MCP Apps bridge first, and keeps window.openai as a compatibility extension. It has no external script or network dependency. The widget provides a read-only organization membership and permission matrix.
@@ -0,0 +1,617 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SHD MCP</title>
<style>
:root{color-scheme:light dark;--bg:#f8fafc;--surface:#e8edf2;--hover:#e1e8ef;--line:rgba(31,41,55,.16);--ink:#1d1f25;--muted:#616670;--accent:#535963;--soft:rgba(75,85,99,.12);--bad:#b42318;--good:#2f855a;--button:#fff;--shadow:0 10px 26px rgba(31,41,55,.1)}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.shell{max-width:980px;margin:auto;padding:22px}.card{padding:20px;border:1px solid var(--line);border-radius:18px;background:var(--surface);box-shadow:var(--shadow)}
.eyebrow{color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}h1{margin:4px 0 5px;font-size:25px;line-height:1.15}.lede{margin:0;color:var(--muted)}.summary{display:flex;gap:8px;flex-wrap:wrap;margin:18px 0 14px}.summary span{padding:5px 9px;border:1px solid var(--line);border-radius:999px;background:var(--hover);color:var(--muted)}.summary strong{color:var(--ink)}
.toolbar{display:flex;gap:8px;margin-bottom:10px}.search{min-width:0;flex:1;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:inherit;outline:0}.search:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--soft)}button{cursor:pointer}.refresh{padding:10px 14px;border:1px solid var(--accent);border-radius:10px;background:var(--accent);color:var(--button);font-weight:650}.refresh:disabled{opacity:.6;cursor:wait}.status{min-height:22px;color:var(--muted)}.error{color:var(--bad)}
.table-wrap{max-height:460px;overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--bg)}table{width:100%;border-collapse:collapse;min-width:560px}th,td{padding:11px 13px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}th{position:sticky;top:0;background:var(--hover);color:var(--muted);font-size:12px;font-weight:650;z-index:1}tbody tr:last-child td{border-bottom:0}.record{cursor:pointer;outline:0}.record:hover,.record:focus{background:var(--soft)}.title{font-weight:650}.sub{display:block;color:var(--muted);font-size:12px;margin-top:2px}.pill{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--soft);color:var(--accent);font-size:12px;white-space:nowrap}.pill.up,.pill.ok,.pill.active{color:var(--good);background:rgba(47,133,90,.14)}.pill.down,.pill.error,.pill.overdue{color:var(--bad);background:rgba(180,35,24,.12)}.empty{padding:24px;text-align:center;color:var(--muted)}
.visual[hidden]{display:none}.visual{max-height:460px;overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--bg);padding:12px}.visual-card{display:block;width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:10px;background:var(--surface);color:inherit;text-align:left;cursor:pointer}.visual-card:hover,.visual-card:focus{background:var(--hover);border-color:var(--accent);outline:0}.visual-title{display:block;font-weight:650}.visual-date,.visual-value{display:block;color:var(--muted);font-size:12px;margin-top:4px}.visual-line{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:7px}.kanban{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(190px,1fr);gap:10px;overflow-x:auto;align-items:start}.kanban-col{min-width:190px;padding:10px;border:1px solid var(--line);border-radius:12px;background:var(--surface)}.kanban-head{display:flex;justify-content:space-between;gap:8px;margin-bottom:8px;font-weight:650}.kanban-list{display:grid;gap:8px}.metric-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(145px,1fr));gap:10px;margin-bottom:12px}.metric{padding:12px;border:1px solid var(--line);border-radius:10px;background:var(--surface)}.metric-label{display:block;color:var(--muted);font-size:12px}.metric-value{display:block;font-size:22px;font-weight:700;margin-top:4px}.timeline{display:grid;gap:0}.timeline-item{display:grid;grid-template-columns:18px minmax(0,1fr);gap:10px;position:relative;padding-bottom:10px}.timeline-rail{position:absolute;left:8px;top:16px;bottom:0;width:2px;background:var(--line)}.timeline-item:last-child .timeline-rail{display:none}.timeline-dot{width:16px;height:16px;border-radius:50%;background:var(--accent);border:3px solid var(--soft);z-index:1}.timeline-content{min-width:0}.timeline-meta{color:var(--muted);font-size:12px;margin-bottom:5px}.funnel{display:grid;gap:9px}.funnel-row{display:grid;grid-template-columns:minmax(100px,1fr) minmax(120px,2fr) auto;gap:10px;align-items:center}.funnel-fill{width:100%;height:10px;accent-color:var(--accent)}.progress-card{display:grid;gap:7px;padding:11px;border:1px solid var(--line);border-radius:10px;background:var(--surface)}.progress{width:100%;height:10px;accent-color:var(--accent)}.calendar{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}.calendar-day{padding:10px;border:1px solid var(--line);border-radius:12px;background:var(--surface);display:grid;gap:8px;align-content:start}.calendar-head{font-weight:650}.matrix-wrap{overflow:auto}.matrix-table{width:100%;border-collapse:collapse;min-width:560px}.matrix-table th,.matrix-table td{padding:9px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}.matrix-table th{color:var(--muted);font-size:12px;background:var(--hover)}.tree{display:grid;gap:7px}.tree-children{display:grid;gap:7px;padding-left:18px;border-left:1px solid var(--line)}.preview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}.preview-card{padding:12px;border:1px solid var(--line);border-radius:10px;background:var(--surface)}.preview-fields{display:grid;gap:5px;margin-top:8px}.preview-field{font-size:12px;overflow-wrap:anywhere}.preview-field strong{color:var(--muted);font-weight:500}@media(max-width:640px){.funnel-row{grid-template-columns:1fr auto}.funnel-fill{grid-column:1/-1}}
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
</style>
<style data-shd-widget-write-actions="css">
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
.shd-widget-actions[hidden]{display:none}
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
.shd-widget-actions-field:has(textarea){grid-column:span 2}
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
.shd-widget-actions-danger{color:var(--danger,#b42318)}
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
</style>
</head>
<body>
<main class="shell" data-widget-module="acl-matrix"><section class="card">
<div class="eyebrow">SHD MCP</div><h1 id="title">SHD</h1><p class="lede" id="desc"></p><div class="summary" id="summary"></div>
<div class="toolbar"><input class="search" id="search" type="search" autocomplete="off"><button class="refresh" id="refresh" type="button"></button></div>
<div class="status" id="status" role="status" aria-live="polite"></div>
<div class="table-wrap"><table><thead><tr><th id="head-title"></th><th id="head-status"></th><th id="head-date"></th><th id="head-value"></th></tr></thead><tbody id="rows"></tbody></table><div class="empty" id="empty" hidden></div></div><div class="visual" id="visual" hidden></div>
<section class="details" id="details" hidden aria-live="polite"><div class="detail-head"><div><h2 id="detail-title"></h2><div class="sub" id="detail-sub"></div></div><button class="close" id="close" type="button">×</button></div><div class="grid" id="grid"></div></section>
</section></main>
<script>
(function(){
'use strict';
var DEFAULT_MODULE='acl-matrix';
var C={ru:{refresh:'Обновить',search:'Поиск по названию, статусу или ID',loading:'Загрузка…',empty:'Нет данных для отображения.',records:'записей',status:'Статус',date:'Дата',value:'Детали',id:'ID',details:'Детали',close:'Закрыть',load:'Не удалось загрузить данные.',detail:'Не удалось загрузить детали.',context:'Для обновления нужен контекст источника',total:'Всего',amount:'Сумма',planned:'План',capacity:'Ёмкость',utilization:'Загрузка',overload:'Перегрузка',warnings:'Предупреждения',complete:'Готово',pending:'Ожидают',approved:'Согласовано',rejected:'Отклонено',progress:'Прогресс',score:'Оценка',member:'Участник',role:'Роль',permissions:'Разрешения',preview:'Предпросмотр',noWarnings:'Предупреждений нет.',health:'Состояние'},en:{refresh:'Refresh',search:'Search by name, status or ID',loading:'Loading…',empty:'No data to display.',records:'records',status:'Status',date:'Date',value:'Details',id:'ID',details:'Details',close:'Close',load:'Could not load data.',detail:'Could not load details.',context:'A source context is required to refresh.',total:'Total',amount:'Amount',planned:'Planned',capacity:'Capacity',utilization:'Utilization',overload:'Overload',warnings:'Warnings',complete:'Complete',pending:'Pending',approved:'Approved',rejected:'Rejected',progress:'Progress',score:'Score',member:'Member',role:'Role',permissions:'Permissions',preview:'Preview',noWarnings:'No warnings.',health:'Health'}};
var M={
discussions:{ru:['Обсуждения','Темы обсуждений с поиском и просмотром сообщений.','Тема','Обновлено','Сообщение'],en:['Discussions','Discussion topics with search and message details.','Topic','Updated','Message']},
notifications:{ru:['Уведомления','Последние уведомления и события модулей.','Уведомление','Время','Источник'],en:['Notifications','Recent notifications and module events.','Notification','Time','Source']},
scheduling:{ru:['Расписание','Ближайшие события и записи из расписания.','Событие','Начало','Участник'],en:['Scheduling','Upcoming events and bookings from scheduling.','Event','Starts','Guest']},
inventory:{ru:['Склад','Складские позиции, остатки и статусы.','Позиция','Обновлено','Количество'],en:['Inventory','Stock items, quantities and statuses.','Item','Updated','Quantity']},
agents:{ru:['Агенты и контроллеры','Доступные агенты, контроллеры и их текущее состояние.','Агент или контроллер','Проверено','Тип'],en:['Agents & Controllers','Available agents, controllers and their current state.','Agent or controller','Checked','Type']},
'status-page':{ru:['Status Page','Состояние доступности сервисов и мониторов.','Монитор','Проверено','Доступность'],en:['Status Page','Service availability and monitor state.','Monitor','Checked','Availability']},
'project-db':{ru:['Project DB','Каталог таблиц Project DB с просмотром схемы.','Таблица','Обновлено','Записей'],en:['Project DB','Project DB table catalog with schema details.','Table','Updated','Records']},
notes:{ru:['Заметки','Страницы Notes с поиском и просмотром содержимого.','Страница','Обновлено','Пространство'],en:['Notes','Notes pages with search and content details.','Page','Updated','Space']},
'project-overview':{ru:['Обзор проекта','Сводные данные по разделам выбранного проекта.','Раздел','Обновлено','Элементы'],en:['Project overview','Cross-module records for the selected project.','Section','Updated','Items']},
files:{ru:['Файлы','Файлы выбранного проекта с поиском и безопасным просмотром ссылок.','Файл','Обновлено','Размер или тип'],en:['Files','Files from the selected project with search and safe links.','File','Updated','Size or type']},
proposals:{ru:['Предложения','Предложения и сметы с поиском, статусами и деталями.','Предложение','Обновлено','Сумма или номер'],en:['Proposals','Proposals with search, statuses and details.','Proposal','Updated','Amount or number']},
terms:{ru:['Terms / ТЗ','Документы технического задания и их ревизии.','Документ','Обновлено','Страницы или ревизия'],en:['Terms documents','Terms of Reference documents and revisions.','Document','Updated','Pages or revision']},
activity:{ru:['Активность','Авторизованные realtime-события SHD по выбранному scope.','Событие','Время','Модуль или тип'],en:['Activity','Authenticated SHD realtime events for the selected scope.','Event','Time','Module or type']},
organizations:{ru:['Организации и доступ','Организации, роли и безопасный просмотр участников.','Организация','Обновлено','Роль или тип'],en:['Organizations & access','Organizations, roles and safe membership details.','Organization','Updated','Role or type']},
gitea:{ru:['Gitea Issues','Открытые задачи разрешённого site или project репозитория.','Issue','Обновлено','Состояние'],en:['Gitea Issues','Open issues for an allowed site or project repository.','Issue','Updated','State']},
schemes:{ru:['Схемы','Документы PDF Scheme Editor и их текущее состояние.','Схема','Обновлено','Ревизия или страницы'],en:['Schemes','PDF Scheme Editor documents and their current state.','Scheme','Updated','Revision or pages']},
'task-kanban':{ru:['Задачи Kanban','Задачи по колонкам статусов с поиском и деталями.','Задача','Срок','Исполнитель'],en:['Task Kanban','Tasks grouped by workflow status with search and details.','Task','Due','Assignee']},
'team-workload':{ru:['Загрузка команды','Плановая нагрузка, ёмкость и утилизация команды.','Участник','Период','Часы или процент'],en:['Team workload','Planned workload, capacity and team utilization.','Member','Period','Hours or percent']},
'project-timeline':{ru:['Timeline проекта','Задачи проекта на временной ленте.','Задача','Срок','Статус'],en:['Project timeline','Project tasks on a chronological timeline.','Task','Due','Status']},
'crm-funnel':{ru:['CRM-воронка','Сделки CRM, сгруппированные по стадиям.','Стадия','Сделки','Сумма'],en:['CRM funnel','CRM deals grouped by pipeline stage.','Stage','Deals','Amount']},
'finance-dashboard':{ru:['Финансовый дашборд','Суммы, статусы и документы финансового модуля.','Документ','Обновлено','Сумма'],en:['Finance dashboard','Finance amounts, statuses and documents.','Document','Updated','Amount']},
'proposal-approvals':{ru:['Согласования предложений','Очередь предложений и текущие approval-статусы.','Предложение','Обновлено','Approval'],en:['Proposal approvals','Proposal queue and current approval states.','Proposal','Updated','Approval']},
'documents-completeness':{ru:['Полнота документов','Score, ревизии и состояние Terms-документов.','Документ','Обновлено','Полнота'],en:['Documents completeness','Scores, revisions and Terms document state.','Document','Updated','Completeness']},
'agents-health':{ru:['Здоровье агентов','Состояние агентов и контроллеров.','Агент или контроллер','Проверено','Health'],en:['Agents health','Agent and controller health state.','Agent or controller','Checked','Health']},
'schedule-calendar':{ru:['Календарь расписания','Ближайшие записи, сгруппированные по дням.','Событие','День','Участник'],en:['Schedule calendar','Upcoming bookings grouped by day.','Event','Day','Guest']},
'acl-matrix':{ru:['ACL-матрица','Участники организации, роли и permissions.','Участник','Роль','Права'],en:['ACL matrix','Organization members, roles and permissions.','Member','Role','Permissions']},
'inventory-warnings':{ru:['Предупреждения склада','Низкие остатки и критические складские позиции.','Позиция','Обновлено','Остаток'],en:['Inventory warnings','Low-stock and critical inventory items.','Item','Updated','Stock']},
'gitea-board':{ru:['Gitea-доска','Issues по состояниям разрешённого репозитория.','Issue','Состояние','Ссылка'],en:['Gitea board','Issues grouped by state for an allowed repository.','Issue','State','Link']},
'schemes-progress':{ru:['Прогресс схем','Документы схем, ревизии, страницы и completeness.','Схема','Обновлено','Прогресс'],en:['Schemes progress','Scheme documents, revisions, pages and completeness.','Scheme','Updated','Progress']},
'notes-tree':{ru:['Дерево Notes','Пространства Notes и их иерархия.','Узел','Обновлено','Тип'],en:['Notes tree','Notes spaces and their hierarchy.','Node','Updated','Type']},
'project-db-preview':{ru:['Project DB preview','Предпросмотр записей выбранной таблицы Project DB.','Запись','Обновлено','Поля'],en:['Project DB preview','Preview of records from the selected Project DB table.','Record','Updated','Fields']}
};
var S={
discussions:{listTool:'shd_list_discussion_topics',listArgs:{limit:100},readTool:'shd_list_discussion_messages',detailKey:'topic_id',detailArgs:{limit:50},paths:['data'],sort:'desc'},
notifications:{listTool:'shd_list_notifications',listArgs:{limit:50,offset:0},paths:['data'],sort:'desc'},
scheduling:{listTool:'shd_get_scheduling_overview',listArgs:{limit:100},readTool:'shd_list_scheduling_bookings',detailKey:'booking_id',detailArgs:{limit:1},paths:['data.bookings','data.upcoming_bookings','data'],sort:'asc'},
inventory:{listTool:'shd_list_inventory_items',listArgs:{limit:100},readTool:'shd_get_inventory_item',detailKey:'item_id',paths:['data'],sort:'desc'},
agents:{listTool:'shd_agents_overview',listArgs:{},paths:['data.controllers','data.agents','data.user_agents','data'],sort:'asc'},
'status-page':{listTool:'shd_get_status_page_monitors',listArgs:{environment:'dev'},readTool:'shd_get_status_page_monitor_history',detailKey:'monitor_id',detailArgs:{limit:20},paths:['data'],nested:'monitors',sort:'asc'},
'project-db':{listTool:'shd_project_db_tables',listArgs:{},readTool:'shd_project_db_schema',detailKey:'table',paths:['data.tables','data'],contextRequired:true,sort:'asc'},
notes:{listTool:'shd_list_note_pages',listArgs:{limit:100},readTool:'shd_get_note_page',detailKey:'page_id',paths:['data'],sort:'desc'},
'project-overview':{listTool:'shd_get_project_context',listArgs:{include:['summary','tasks','discussions','documents','finance','terms','agents'],limit_per_section:100},paths:['data.sections.tasks','data.sections.discussions','data.sections.documents','data.sections.finance','data.sections.terms'],combinePaths:true,contextRequired:true,sort:'desc'},
files:{listTool:'shd_list_files',readTool:'shd_get_file_link',detailKey:'path',detailArgs:{download:false},detailSourceArgs:['project_code'],paths:['data'],contextRequired:true,sort:'desc'},
proposals:{listTool:'shd_list_proposals',listArgs:{limit:100},readTool:'shd_get_proposal',detailKey:'proposal_id',detailSourceArgs:[],paths:['data'],sort:'desc'},
terms:{listTool:'shd_list_terms_documents',readTool:'shd_get_terms_document',detailKey:'document_id',detailSourceArgs:['project_code'],paths:['data'],contextRequired:true,sort:'desc'},
activity:{listTool:'shd_get_realtime_events',listArgs:{module:'notifications',limit:100},paths:['data.events','events','data'],sort:'desc'},
organizations:{listTool:'shd_admin_list_organizations',listArgs:{limit:100},readTool:'shd_admin_get_organization_members',detailKey:'organization_id',detailSourceArgs:[],paths:['data'],sort:'asc'},
gitea:{listTool:'shd_list_gitea_issues',listArgs:{target:'site',limit:100},paths:['data'],sort:'desc'},
schemes:{listTool:'shd_list_scheme_documents',readTool:'shd_get_scheme_document',detailKey:'document',detailSourceArgs:['project'],paths:['data'],contextRequired:true,sort:'desc'},
'task-kanban':{listTool:'shd_list_tasks',listArgs:{limit:100},readTool:'shd_get_task',detailKey:'task_id',detailSourceArgs:[],paths:['data'],view:'kanban',groupBy:'status',sort:'asc'},
'team-workload':{listTool:'shd_get_task_workload',listArgs:{period:'month'},paths:['data.rows','data.workload','data.assignees','data'],view:'workload',sort:'desc'},
'project-timeline':{listTool:'shd_list_tasks',listArgs:{limit:100},paths:['data'],view:'timeline',sort:'asc'},
'crm-funnel':{listTool:'shd_list_crm',listArgs:{section:'deals',limit:100},readTool:'shd_get_crm_entity',detailKey:'entity_id',detailSourceArgs:['section'],paths:['data'],view:'funnel',groupBy:'stage',sort:'desc'},
'finance-dashboard':{listTool:'shd_list_finance_documents',listArgs:{limit:100},paths:['data'],view:'dashboard',sort:'desc'},
'proposal-approvals':{listTool:'shd_list_proposals',listArgs:{limit:100},readTool:'shd_get_proposal_approval',detailKey:'proposal_id',detailSourceArgs:[],paths:['data'],view:'approval',sort:'desc'},
'documents-completeness':{listTool:'shd_list_terms_documents',readTool:'shd_get_terms_document_completeness',detailKey:'document_id',detailSourceArgs:['project_code'],paths:['data'],view:'completeness',contextRequired:true,contextKeys:['project_code'],sort:'desc'},
'agents-health':{listTool:'shd_agents_overview',paths:['data.controllers','data.agents','data.user_agents','data'],view:'health',sort:'asc'},
'schedule-calendar':{listTool:'shd_get_scheduling_overview',listArgs:{limit:100},readTool:'shd_list_scheduling_bookings',detailKey:'booking_id',detailArgs:{limit:1},paths:['data.bookings','data.upcoming_bookings','data'],view:'calendar',sort:'asc'},
'acl-matrix':{listTool:'shd_list_organization_members',listArgs:{limit:100},paths:['data'],view:'matrix',contextRequired:true,contextKeys:['organization_id'],contextAll:true,sort:'asc'},
'inventory-warnings':{listTool:'shd_list_inventory_items',listArgs:{limit:100},readTool:'shd_get_inventory_item',detailKey:'item_id',detailSourceArgs:[],paths:['data'],view:'warnings',sort:'asc'},
'gitea-board':{listTool:'shd_list_gitea_issues',listArgs:{target:'site',limit:100},paths:['data'],view:'kanban',groupBy:'state',sort:'asc'},
'schemes-progress':{listTool:'shd_list_scheme_documents',readTool:'shd_get_scheme_document',detailKey:'document',detailSourceArgs:['project'],paths:['data'],view:'progress',contextRequired:true,contextKeys:['project'],sort:'desc'},
'notes-tree':{listTool:'shd_list_note_spaces',listArgs:{limit:100},readTool:'shd_get_note_space_tree',detailKey:'space_id',detailSourceArgs:[],paths:['data'],view:'tree',sort:'asc'},
'project-db-preview':{listTool:'shd_project_db_records',listArgs:{limit:100,include_fields:true,include_view_meta:true},paths:['data.records','data'],view:'preview',contextRequired:true,contextKeys:['base','project_code','table'],contextAll:true,sort:'asc'}
};
var pending=new Map(),nextId=1,rows=[],state={module:DEFAULT_MODULE,config:Object.assign({},S[DEFAULT_MODULE]),lang:'ru',busy:false};
var e={title:document.getElementById('title'),desc:document.getElementById('desc'),search:document.getElementById('search'),refresh:document.getElementById('refresh'),status:document.getElementById('status'),summary:document.getElementById('summary'),tableWrap:document.querySelector('.table-wrap'),visual:document.getElementById('visual'),rows:document.getElementById('rows'),empty:document.getElementById('empty'),details:document.getElementById('details'),detailTitle:document.getElementById('detail-title'),detailSub:document.getElementById('detail-sub'),grid:document.getElementById('grid'),close:document.getElementById('close'),ht:document.getElementById('head-title'),hs:document.getElementById('head-status'),hd:document.getElementById('head-date'),hv:document.getElementById('head-value')};
function lang(){var x=typeof window!=='undefined'&&window.openai&&window.openai.locale;x=String(x||document.documentElement.lang||'ru').toLowerCase();return x.indexOf('en')===0?'en':'ru'}
function t(k){return(C[state.lang]||C.ru)[k]||k}
function obj(x){return x&&typeof x==='object'&&!Array.isArray(x)}
function merge(a,b){return Object.assign({},a||{},b||{})}
function payload(x){if(!x)return null;if(Object.prototype.hasOwnProperty.call(x,'structuredContent'))return x.structuredContent;if(Object.prototype.hasOwnProperty.call(x,'toolOutput'))return x.toolOutput;return x}
function unwrap(x){if(x&&x.isError)throw Error(t('load'));var p=payload(x)||{};if(p.error)throw Error(String(p.error.message||p.error.detail||p.error));if(p.ok===false&&p.message)throw Error(String(p.message));return p}
function applyConfig(p){p=payload(p)||{};var m=obj(p.meta)?p.meta:{},w=obj(m.widget)?m.widget:{},mod=String(w.module||m.module||state.module||DEFAULT_MODULE);state.module=M[mod]?mod:DEFAULT_MODULE;state.config=merge(S[state.module],w);if(!obj(state.config.sourceArgs)&&obj(m.sourceArgs))state.config.sourceArgs=m.sourceArgs;state.lang=lang();labels()}
function labels(){var m=(M[state.module]||M.discussions)[state.lang]||M.discussions.ru;document.documentElement.lang=state.lang;e.title.textContent=m[0];e.desc.textContent=m[1];e.search.placeholder=t('search');e.refresh.textContent=t('refresh');e.empty.textContent=t('empty');e.close.setAttribute('aria-label',t('close'));e.ht.textContent=m[2];e.hs.textContent=t('status');e.hd.textContent=m[3];e.hv.textContent=m[4]}
function req(method,params){var o=typeof window!=='undefined'&&window.openai;if(method==='tools/call'&&o&&typeof o.callTool==='function')return Promise.resolve(o.callTool(params.name,params.arguments||{}));var id=nextId++;window.parent.postMessage({jsonrpc:'2.0',id:id,method:method,params:params||{}},'*');return new Promise(function(resolve,reject){var timeout=window.setTimeout(function(){pending.delete(id);reject(Error(t('load')))},12000);pending.set(id,{resolve:resolve,reject:reject,timeout:timeout})})}
function notify(method,params){window.parent.postMessage({jsonrpc:'2.0',method:method,params:params||{}},'*')}
function path(x,p){return String(p||'').split('.').reduce(function(v,k){return v&&typeof v==='object'?v[k]:undefined},x)}
function findArray(x,d){if(Array.isArray(x))return x;if(!obj(x)||d>2)return null;var keys=['items','records','bookings','upcoming_bookings','topics','notifications','pages','spaces','controllers','agents','user_agents','tables','monitors','data'];for(var i=0;i<keys.length;i++)if(Array.isArray(x[keys[i]]))return x[keys[i]];for(var k in x)if(Object.prototype.hasOwnProperty.call(x,k)){var a=findArray(x[k],d+1);if(a)return a}return null}
function flatten(a){var n=state.config.nested||state.config.nestedCollection;if(!n)return a;var out=[];a.forEach(function(parent){if(parent&&Array.isArray(parent[n]))parent[n].forEach(function(child){var x=Object.assign({},child);if(parent.name&&!x.group_name)x.group_name=parent.name;out.push(x)});else out.push(parent)});return out}
function list(x){var p=payload(x)||{};if(Array.isArray(p.items))return p.items;var ps=Array.isArray(state.config.paths)?state.config.paths:(Array.isArray(state.config.arrayPaths)?state.config.arrayPaths:[]),combined=[];for(var i=0;i<ps.length;i++){var c=path(p,ps[i]),a=Array.isArray(c)?c:findArray(c,0);if(a){if(state.config.combinePaths)combined=combined.concat(a);else return flatten(a)}}if(state.config.combinePaths&&combined.length)return flatten(combined);var d=p.data===undefined?p:p.data;return flatten(Array.isArray(d)?d:(findArray(d,0)||[]))}
function str(x){if(x===null||x===undefined||x==='')return'';if(typeof x==='object'){try{return JSON.stringify(x)}catch(err){return''}}return String(x)}
function id(x,i){var keys=['id','key','code','uuid','page_id','topic_id','booking_id','item_id','monitor_id','table'];for(var n=0;n<keys.length;n++)if(x&&x[keys[n]]!==undefined&&x[keys[n]]!==null&&x[keys[n]]!=='')return str(x[keys[n]]);return String(i+1)}
function title(x,i){return str(x&&(x.title||x.name||x.subject||x.display_name||x.label||x.table_name||x.table||x.key))||(t('records')+' '+(i+1))}
function date(x){return str(x&&(x.starts_at||x.start_at||x.due_date||x.checked_at||x.updated_at||x.created_at||x.sent_at||x.published_at||x.date))}
function value(x){if(!x)return'';if(x.availability_percent!==undefined&&x.availability_percent!==null)return str(x.availability_percent)+'%';if(x.quantity!==undefined&&x.quantity!==null)return str(x.quantity);if(x.record_count!==undefined&&x.record_count!==null)return str(x.record_count);return str(x.value!==undefined?x.value:(x.guest_name||x.space_name||x.group_name||x.controller_type||x.type||x.source||x.module||''))}
function sub(x){return str(x&&(x.subtitle||x.project_code||x.space_name||x.channel_name||x.group_name||x.code||x.email))}
function normalize(x,i){var y=obj(x)?Object.assign({},x):{value:x};y.id=id(x,i);y.title=title(x,i);y.subtitle=sub(x);y.status=status(x);y.date=date(x);y.value=value(x);return y}
function dateText(x){if(!x)return'—';var n=Date.parse(x);if(Number.isNaN(n))return String(x);try{return new Intl.DateTimeFormat(state.lang,{dateStyle:'medium',timeStyle:'short'}).format(new Date(n))}catch(err){return String(x)}}
function make(tag,v,cls){var n=document.createElement(tag);if(cls)n.className=cls;if(v!==undefined)n.textContent=v;return n}
function detailText(x){return x===null||x===undefined||x===''?'—':str(x)}
function renderSummary(){e.summary.replaceChildren();var n=make('span');n.append(make('strong',String(rows.length)),' '+t('records'));e.summary.append(n);var c={};rows.forEach(function(x){if(x.status)c[x.status]=(c[x.status]||0)+1});Object.keys(c).slice(0,3).forEach(function(k){var n=make('span');n.append(make('strong',String(c[k])),' '+k);e.summary.append(n)})}
function field(x,keys){for(var i=0;i<keys.length;i++){var v=x&&x[keys[i]];if(v!==undefined&&v!==null&&v!=='')return v}return''}
function numeric(x,keys){var v=field(x,keys);if(v===null||v===undefined||v==='')return null;if(typeof v==='number')return Number.isFinite(v)?v:null;var n=Number(String(v).replace(/,/g,'.').replace(/[^0-9.-]/g,''));return Number.isFinite(n)?n:null}
function groupValue(x,key){var v=key?field(x,[key]):status(x);if(obj(v))v=v.name||v.label||v.code||v.id;return str(v)||'—'}
function visualCard(x,extra){var b=make('button',undefined,'visual-card'+(extra?' '+extra:''));b.type='button';b.append(make('span',x.title||'SHD','visual-title'));if(x.subtitle)b.append(make('span',x.subtitle,'sub'));var line=make('span',undefined,'visual-line');line.append(make('span',x.status||'—','pill'));if(x.date)line.append(make('span',dateText(x.date),'visual-date'));b.append(line);var v=field(x,['total_amount','amount','planned_hours','utilization_percent','score','quantity','revision','pages','value']);if(v!==''&&v!==null)b.append(make('span',str(v),'visual-value'));b.onclick=function(){details(x)};return b}
function visualEmpty(){e.visual.replaceChildren(make('div',t('empty'),'empty'))}
function renderKanban(a){if(!a.length){visualEmpty();return}var groups={};a.forEach(function(x){var g=groupValue(x,state.config.groupBy||'status');(groups[g]||(groups[g]=[])).push(x)});var board=make('div',undefined,'kanban');Object.keys(groups).forEach(function(g){var col=make('section',undefined,'kanban-col');var head=make('div',undefined,'kanban-head');head.append(make('span',g),make('span',String(groups[g].length),'pill'));col.append(head);var list=make('div',undefined,'kanban-list');groups[g].forEach(function(x){list.append(visualCard(x))});col.append(list);board.append(col)});e.visual.append(board)}
function renderTimeline(a){if(!a.length){visualEmpty();return}var wrap=make('div',undefined,'timeline');a.slice().sort(function(x,y){return String(x.date||'').localeCompare(String(y.date||''))}).forEach(function(x){var item=make('article',undefined,'timeline-item'),rail=make('span',undefined,'timeline-rail'),dot=make('span',undefined,'timeline-dot'),content=make('div',undefined,'timeline-content');content.append(make('div',dateText(x.date),'timeline-meta'),visualCard(x,'timeline-card'));item.append(rail,dot,content);wrap.append(item)});e.visual.append(wrap)}
function renderFunnel(a){if(!a.length){visualEmpty();return}var groups={},amounts={};a.forEach(function(x){var g=groupValue(x,state.config.groupBy||'stage');groups[g]=(groups[g]||0)+1;amounts[g]=(amounts[g]||0)+(numeric(x,['total_amount','amount','value','sum'])||0)});var max=Math.max.apply(null,Object.values(groups));var wrap=make('div',undefined,'funnel');Object.keys(groups).forEach(function(g){var row=make('div',undefined,'funnel-row'),bar=document.createElement('progress');bar.className='funnel-fill';bar.max=max||1;bar.value=groups[g];row.append(make('span',g),bar,make('span',String(groups[g])+(amounts[g]?' · '+String(amounts[g]):'')));wrap.append(row)});e.visual.append(wrap)}
function metric(label,value){var d=make('div',undefined,'metric');d.append(make('span',label,'metric-label'),make('span',value===null||value===undefined?'—':str(value),'metric-value'));return d}
function renderDashboard(a,mode){var grid=make('div',undefined,'metric-grid'),total=a.length,amount=0,planned=0,capacity=0,util=0,overload=0;a.forEach(function(x){amount+=numeric(x,['total_amount','amount','total','sum','value'])||0;planned+=numeric(x,['planned_hours','planned','hours'])||0;capacity+=numeric(x,['capacity_hours','capacity'])||0;util+=numeric(x,['utilization_percent','utilization'])||0;overload+=numeric(x,['overload_hours','overload'])||0});grid.append(metric(t('total'),total));if(mode==='workload'){grid.append(metric(t('planned'),planned||null),metric(t('capacity'),capacity||null),metric(t('utilization'),a.length?Math.round(util/a.length)+'%':null),metric(t('overload'),overload||0))}else{grid.append(metric(t('amount'),amount||null),metric(t('status'),Object.keys(a.reduce(function(m,x){m[groupValue(x,'status')]=1;return m},{})).length))}e.visual.append(grid);if(!a.length){visualEmpty();return}var list=make('div',undefined,'kanban-list');a.slice(0,60).forEach(function(x){list.append(visualCard(x))});e.visual.append(list)}
function approvalValue(x){return groupValue(x,'approval_status')!=='—'?groupValue(x,'approval_status'):groupValue(x,'approval_state')!=='—'?groupValue(x,'approval_state'):groupValue(x,'status')}
function renderStatusSummary(a,keys){if(!a.length){visualEmpty();return}var counts={};a.forEach(function(x){var g=groupValue(x,keys[0]);if(g==='—')g=groupValue(x,keys[1]);if(g==='—')g=groupValue(x,'status');counts[g]=(counts[g]||0)+1});var grid=make('div',undefined,'metric-grid');Object.keys(counts).slice(0,6).forEach(function(g){grid.append(metric(g,counts[g]))});e.visual.append(grid);var list=make('div',undefined,'kanban-list');a.slice(0,60).forEach(function(x){list.append(visualCard(x))});e.visual.append(list)}
function progressValue(x){var n=numeric(x,['score','completeness_score','completion_percent','progress','availability_percent']);if(n!==null)return Math.max(0,Math.min(100,n));if(x.complete===true||x.is_complete===true||String(status(x)).toLowerCase()==='complete')return 100;return 0}
function renderProgress(a){if(!a.length){visualEmpty();return}var grid=make('div',undefined,'preview-grid');a.forEach(function(x){var card=make('article',undefined,'progress-card');card.append(make('strong',x.title||'SHD','visual-title'));var meta=make('span',x.subtitle||dateText(x.date),'visual-date');card.append(meta);var p=document.createElement('progress');p.className='progress';p.max=100;p.value=progressValue(x);card.append(p,make('span',t('progress')+': '+String(p.value)+'%','visual-value'));card.onclick=function(){details(x)};grid.append(card)});e.visual.append(grid)}
function isWarning(x){var s=String(status(x)).toLowerCase();if(x.low_stock||x.is_low_stock||x.critical||x.warning)return true;if(/low|critical|out|error|overdue|maintenance/.test(s))return true;var q=numeric(x,['quantity','available_quantity','stock']);var min=numeric(x,['min_quantity','minimum_quantity','reorder_level']);return q!==null&&min!==null&&q<=min}
function renderWarnings(a){var warnings=a.filter(isWarning),grid=make('div',undefined,'metric-grid');grid.append(metric(t('warnings'),warnings.length),metric(t('total'),a.length));e.visual.append(grid);if(!warnings.length){e.visual.append(make('div',t('noWarnings'),'empty'));return}var list=make('div',undefined,'kanban-list');warnings.forEach(function(x){list.append(visualCard(x,'warning'))});e.visual.append(list)}
function renderCalendar(a){if(!a.length){visualEmpty();return}var days={};a.forEach(function(x){var d=x.date?String(x.date).slice(0,10):'—';(days[d]||(days[d]=[])).push(x)});var cal=make('div',undefined,'calendar');Object.keys(days).forEach(function(d){var day=make('section',undefined,'calendar-day');day.append(make('div',d,'calendar-head'));days[d].forEach(function(x){day.append(visualCard(x))});cal.append(day)});e.visual.append(cal)}
function memberName(x){var v=field(x,['member_name','username','email','name','display_name','user']);if(obj(v))return str(v.name||v.username||v.email||v.id);return str(v)||x.id}
function permissionText(x){var p=x.permissions||x.permission||x.scopes||'';if(obj(p))return Object.keys(p).filter(function(k){return p[k]===true||p[k]===1||typeof p[k]==='string'}).map(function(k){return k+(typeof p[k]==='string'?'='+p[k]:'')}).join(', ')||'—';if(Array.isArray(p))return p.join(', ')||'—';return str(p)||'—'}
function renderMatrix(a){if(!a.length){visualEmpty();return}var wrap=make('div',undefined,'matrix-wrap'),table=make('table',undefined,'matrix-table'),head=make('tr');[t('member'),t('role'),t('status'),t('permissions')].forEach(function(x){head.append(make('th',x))});table.append(make('thead').appendChild(head).parentNode);var body=make('tbody');a.forEach(function(x){var row=make('tr');row.append(make('td',memberName(x)),make('td',str(field(x,['role','organization_role','type']))||'—'),make('td',status(x)||'—'),make('td',permissionText(x)));row.onclick=function(){details(x)};row.className='record';body.append(row)});table.append(body);wrap.append(table);e.visual.append(wrap)}
function treeId(x,i){return str(field(x,['id','space_id','page_id','collection_id','key']))||String(i)}
function treeParent(x){return str(field(x,['parent_id','parentId','parent_space_id','parent_collection_id']))}
function renderTree(a){if(!a.length){visualEmpty();return}var by={};a.forEach(function(x,i){var p=treeParent(x);(by[p]||(by[p]=[])).push({x:x,i:i})});var tree=make('div',undefined,'tree');function branch(parent,host,seen){(by[parent]||[]).forEach(function(entry){var id=treeId(entry.x,entry.i);if(seen[id])return;var wrap=make('div',undefined,'tree-node');wrap.append(visualCard(entry.x));host.append(wrap);var child=make('div',undefined,'tree-children');branch(id,child,Object.assign({},seen,(function(){var o={};o[id]=true;return o})()));if(child.childNodes.length)wrap.append(child)})}branch('',tree,{});if(!tree.childNodes.length)a.forEach(function(x){tree.append(visualCard(x))});e.visual.append(tree)}
function renderPreview(a){if(!a.length){visualEmpty();return}var grid=make('div',undefined,'preview-grid');a.forEach(function(x){var card=make('article',undefined,'preview-card');card.append(make('strong',x.title||'SHD','visual-title'));var fields=make('div',undefined,'preview-fields');Object.keys(x).filter(function(k){return['id','title','subtitle','status','date','value','meta'].indexOf(k)===-1}).slice(0,10).forEach(function(k){var cell=make('div',undefined,'preview-field');cell.append(make('strong',k+': '),make('span',str(x[k])));fields.append(cell)});card.append(fields);card.onclick=function(){details(x)};grid.append(card)});e.visual.append(grid)}
function renderVisual(a){e.visual.replaceChildren();var v=state.config.view||'table';if(v==='kanban')renderKanban(a);else if(v==='workload'||v==='dashboard')renderDashboard(a,v);else if(v==='timeline')renderTimeline(a);else if(v==='funnel')renderFunnel(a);else if(v==='approval')renderStatusSummary(a,['approval_status','approval_state']);else if(v==='completeness'||v==='progress')renderProgress(a);else if(v==='health')renderStatusSummary(a,['health','status']);else if(v==='calendar')renderCalendar(a);else if(v==='matrix')renderMatrix(a);else if(v==='warnings')renderWarnings(a);else if(v==='tree')renderTree(a);else if(v==='preview')renderPreview(a);else renderDashboard(a,'dashboard')}
function renderTable(a){e.rows.replaceChildren();e.empty.hidden=a.length!==0;a.forEach(function(x){var r=document.createElement('tr');r.className='record';r.tabIndex=0;r.setAttribute('role','button');r.setAttribute('aria-label',x.title);var n=make('td');n.append(make('span',x.title,'title'));if(x.subtitle)n.append(make('span',x.subtitle,'sub'));var s=make('span',x.status||'—','pill');if(x.status)s.classList.add(String(x.status).toLowerCase().replace(/[^a-z0-9_-]+/g,'-'));var sc=make('td');sc.append(s);r.append(n,sc,make('td',dateText(x.date)),make('td',x.value||'—'));r.onclick=function(){details(x)};r.onkeydown=function(ev){if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();details(x)}};e.rows.append(r)});renderSummary()}
function renderRows(){var q=e.search.value.trim().toLowerCase(),a=rows.filter(function(x){return!q||[x.title,x.subtitle,x.status,x.id,x.value].map(str).join(' ').toLowerCase().indexOf(q)!==-1});if(state.config.view&&state.config.view!=='table'){e.tableWrap.hidden=true;e.visual.hidden=false;e.empty.hidden=true;renderVisual(a);renderSummary();return}e.tableWrap.hidden=false;e.visual.hidden=true;renderTable(a)}
function render(x){var p=unwrap(x);applyConfig(p);rows=list(p).map(normalize);var dir=state.config.sort==='asc'?1:-1;rows.sort(function(a,b){var x=Date.parse(a.date),y=Date.parse(b.date);if(!Number.isNaN(x)&&!Number.isNaN(y)&&x!==y)return(x-y)*dir;return a.title.localeCompare(b.title,state.lang)*dir});renderRows()}
function setStatus(x,bad){e.status.textContent=x||'';e.status.className=bad?'status error':'status'}
function add(k,v){var d=make('div',undefined,'detail');d.append(make('span',k,'label'),make('span',detailText(v),'value'));e.grid.append(d)}
function show(x){e.detailTitle.textContent=x.title||'SHD';e.detailSub.textContent=x.subtitle||x.id||'';e.grid.replaceChildren();add(t('id'),x.id);if(x.status)add(t('status'),x.status);if(x.date)add(t('date'),dateText(x.date));if(x.value)add(t('value'),x.value);Object.keys(x).filter(function(k){return['id','title','subtitle','status','date','value','meta'].indexOf(k)===-1}).slice(0,8).forEach(function(k){add(k,x[k])});e.details.hidden=false}
function details(x){show(x);if(!state.config.readTool)return;var k=state.config.detailKey;if(!k)return;var v=x[k]===undefined?x.id:x[k];if(v===undefined||v===null||v==='')return;if(state.module==='status-page'&&!/^\d+$/.test(String(v)))return;var a={};if(Array.isArray(state.config.detailSourceArgs)){var ctx=merge(state.config.listArgs,state.config.sourceArgs);state.config.detailSourceArgs.forEach(function(name){if(ctx[name]!==undefined)a[name]=ctx[name]})}else a=merge(state.config.sourceArgs,state.config.detailArgs);a=merge(a,state.config.detailArgs);a[k]=k==='table'?String(v):(Number(v)||v);req('tools/call',{name:state.config.readTool,arguments:a}).then(function(result){var p=unwrap(result),d=p.data===undefined?p:p.data;e.grid.replaceChildren();if(Array.isArray(d)){add(t('records'),d.length);d.slice(0,6).forEach(function(q,i){add(String(i+1),q&&(q.message||q.body||q.content||q.title||q))})}else if(obj(d))Object.keys(d).slice(0,12).forEach(function(q){add(q,d[q])});else add(t('details'),d)}).catch(function(err){setStatus(err.message||t('detail'),true)})}
function contextReady(a){if(!state.config.contextRequired)return true;if(Array.isArray(state.config.contextSets)&&state.config.contextSets.length)return state.config.contextSets.every(function(set){return set.some(function(k){return a[k]!==undefined&&a[k]!==null&&a[k]!==''})});var keys=Array.isArray(state.config.contextKeys)&&state.config.contextKeys.length?state.config.contextKeys:['base','project_code','project'];var ready=state.config.contextAll?keys.every(function(k){return a[k]!==undefined&&a[k]!==null&&a[k]!==''}):keys.some(function(k){return a[k]!==undefined&&a[k]!==null&&a[k]!==''});return ready}
function refresh(){if(state.busy)return;var a=merge(state.config.listArgs,state.config.sourceArgs);if(!contextReady(a)){setStatus(t('context'),true);return}if(!state.config.listTool)return;state.busy=true;e.refresh.disabled=true;setStatus(t('loading'),false);req('tools/call',{name:state.config.listTool,arguments:a}).then(function(x){render(x);setStatus('',false)}).catch(function(err){setStatus(err.message||t('load'),true)}).finally(function(){state.busy=false;e.refresh.disabled=false})}
function init(){return req('ui/initialize',{protocolVersion:'2025-11-25',appInfo:{name:'SHD '+(M[state.module]||M.discussions).ru[0],title:'SHD MCP',version:'1.0.0',websiteUrl:'https://shd.xyz.su'},appCapabilities:{}}).then(function(){notify('ui/notifications/initialized',{})}).catch(function(){})}
window.addEventListener('message',function(ev){if(ev.source!==window.parent)return;var m=ev.data;if(!m||m.jsonrpc!=='2.0')return;if(m.id!==undefined&&pending.has(m.id)){var q=pending.get(m.id);pending.delete(m.id);window.clearTimeout(q.timeout);if(m.error)q.reject(Error(m.error.message||t('load')));else q.resolve(m.result);return}if(m.method==='ui/notifications/tool-input'&&!rows.length){try{render(m.params||{})}catch(err){setStatus(err.message||t('load'),true)}}if(m.method==='ui/notifications/tool-result'){try{render(m.params&&m.params.structuredContent)}catch(err){setStatus(err.message||t('load'),true)}}},{passive:true});
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
}());
</script>
<script data-shd-widget-write-actions="js">
(function () {
'use strict';
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
var copy = {
ru: {
actions: 'Действия',
chooseAction: 'Выберите действие',
selectRecord: 'Сначала выберите запись.',
submit: 'Сохранить',
cancel: 'Отмена',
confirm: 'Подтверждаю действие',
warning: 'Проверьте данные перед записью.',
destructive: 'Операция может изменить или удалить данные.',
required: 'Заполните обязательное поле.',
invalidJson: 'Введите корректный JSON.',
success: 'Изменения сохранены.',
failure: 'Не удалось сохранить изменения.',
loading: 'Сохраняю…',
noActions: 'Для этого виджета нет доступных действий.',
refreshed: 'Список обновлён.',
fieldValue: 'Значение',
},
en: {
actions: 'Actions',
chooseAction: 'Choose an action',
selectRecord: 'Select a record first.',
submit: 'Save',
cancel: 'Cancel',
confirm: 'I confirm this action',
warning: 'Review the values before writing.',
destructive: 'This operation may change or remove data.',
required: 'Fill in the required field.',
invalidJson: 'Enter valid JSON.',
success: 'Changes saved.',
failure: 'Could not save changes.',
loading: 'Saving…',
noActions: 'No actions are available for this widget.',
refreshed: 'List refreshed.',
fieldValue: 'Value',
},
};
var state = {
widget: {},
records: [],
selected: null,
action: null,
inputs: {},
busy: false,
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
};
var pending = new Map();
var nextRequestId = 1;
var elements = {};
function t(key) {
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
}
function object(value) {
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}
function normalize(value) {
if (!value) return null;
if (value.structuredContent) return normalize(value.structuredContent);
if (value.toolOutput) return normalize(value.toolOutput);
if (value.toolInput) return normalize(value.toolInput);
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
return value;
}
function widgetFrom(value) {
var payload = normalize(value) || {};
var meta = object(payload.meta) ? payload.meta : {};
return object(meta.widget) ? meta.widget : {};
}
function recordsFrom(value) {
var payload = normalize(value) || {};
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
if (!Array.isArray(data)) data = payload.data;
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
}
function readPath(value, path) {
if (!value || path === undefined || path === null) return undefined;
var parts = String(path).split('.');
var current = value;
for (var index = 0; index < parts.length; index += 1) {
if (current === undefined || current === null) return undefined;
current = current[parts[index]];
}
return current;
}
function firstValue(paths) {
var list = Array.isArray(paths) ? paths : [paths];
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
var sourceObject = sources[sourceIndex];
if (!sourceObject) continue;
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
var value = readPath(sourceObject, list[pathIndex]);
if (value !== undefined && value !== null && value !== '') return value;
}
}
return undefined;
}
function text(value) {
if (value === undefined || value === null) return '';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
}
function label(value) {
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
return String(value || t('fieldValue'));
}
function actionLabel(action) {
return label(action && action.label) || action.id || action.tool || t('actions');
}
function hasTargets(action) {
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
}
function actionList() {
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
return action && action.tool;
}) : [];
}
function request(method, params) {
var openai = typeof window !== 'undefined' ? window.openai : undefined;
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
return openai.callTool(params.name, params.arguments || {});
}
var id = nextRequestId++;
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
return new Promise(function (resolve, reject) {
var timeout = window.setTimeout(function () {
pending.delete(id);
reject(new Error(t('failure')));
}, 15000);
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
});
}
function unwrap(value) {
var payload = normalize(value);
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
if (payload && payload.error) throw new Error(text(payload.error));
return payload || value;
}
function make(tag, value, className) {
var node = document.createElement(tag);
if (className) node.className = className;
if (value !== undefined) node.textContent = value;
return node;
}
function ensurePanel() {
if (elements.panel) return;
var root = document.querySelector('main') || document.body;
var panel = make('section', undefined, 'shd-widget-actions');
panel.hidden = true;
panel.setAttribute('aria-live', 'polite');
var header = make('div', undefined, 'shd-widget-actions-header');
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
var select = document.createElement('select');
select.className = 'shd-widget-actions-select';
select.setAttribute('aria-label', t('actions'));
var form = make('form', undefined, 'shd-widget-actions-form');
var status = make('div', '', 'shd-widget-actions-status');
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
submit.type = 'submit';
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
cancel.type = 'button';
buttons.append(submit, cancel);
panel.append(header, select, form, status, buttons);
root.append(panel);
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
select.addEventListener('change', function () {
var selected = actionList().find(function (action) { return action.id === select.value; });
state.action = selected || null;
renderForm();
});
cancel.addEventListener('click', function () {
state.action = null;
select.value = '';
renderForm();
});
form.addEventListener('submit', function (event) {
event.preventDefault();
submitAction();
});
}
function inputValue(fieldConfig) {
var value = firstValue(fieldConfig.source || []);
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
return value;
}
function createInput(fieldConfig) {
var type = fieldConfig.type || 'text';
var input;
if (type === 'textarea' || type === 'json') {
input = document.createElement('textarea');
input.rows = type === 'json' ? 4 : 3;
if (type === 'json') input.placeholder = '{ }';
} else if (type === 'select') {
input = document.createElement('select');
(fieldConfig.options || []).forEach(function (option) {
var item = document.createElement('option');
item.value = String(option[0]);
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
input.append(item);
});
} else {
input = document.createElement('input');
input.type = type === 'number' || type === 'email' ? type : 'text';
}
input.className = 'shd-widget-actions-input';
input.name = fieldConfig.name;
var value = inputValue(fieldConfig);
if (type === 'checkbox') {
input.type = 'checkbox';
input.checked = value === true || value === 'true' || value === 1 || value === '1';
} else if (value !== undefined && value !== null) {
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
}
if (fieldConfig.required) input.required = true;
return input;
}
function renderForm() {
ensurePanel();
var selectedActionId = state.action ? state.action.id : '';
elements.select.replaceChildren();
var placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = t('chooseAction');
elements.select.append(placeholder);
actionList().forEach(function (action) {
var option = document.createElement('option');
option.value = action.id;
option.textContent = actionLabel(action);
elements.select.append(option);
});
elements.select.value = selectedActionId;
elements.select.hidden = actionList().length === 0;
elements.form.replaceChildren();
elements.status.textContent = '';
elements.submit.disabled = state.busy;
var action = state.action;
if (!action) {
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
elements.submit.hidden = true;
elements.cancel.hidden = true;
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
}
return;
}
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
elements.submit.hidden = false;
elements.cancel.hidden = false;
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
(action.fields || []).forEach(function (fieldConfig) {
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
var wrapper = make('label', undefined, 'shd-widget-actions-field');
wrapper.append(make('span', label(fieldConfig.label)));
wrapper.append(createInput(fieldConfig));
elements.form.append(wrapper);
});
if (action.confirm) {
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
var confirm = document.createElement('input');
confirm.type = 'checkbox';
confirm.name = '__confirm';
confirm.required = true;
confirmLabel.append(confirm, make('span', t('confirm')));
elements.form.append(confirmLabel);
}
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
elements.form.append(notice);
}
function coerce(value, type) {
if (type === 'number') {
var number = Number(value);
return Number.isFinite(number) ? number : value;
}
if (type === 'checkbox') return Boolean(value);
if (type === 'json') {
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
}
return value;
}
function targetType(name) {
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
}
function targetList(action) {
if (Array.isArray(action.targets)) return action.targets;
return action.target ? [action.target] : [];
}
function collectArguments(action) {
var args = Object.assign({}, action.fixed || {});
(action.context || []).forEach(function (name) {
var value = firstValue([name]);
if (value !== undefined && value !== null && value !== '') args[name] = value;
});
targetList(action).forEach(function (targetConfig) {
var value = firstValue(targetConfig.source || targetConfig.name);
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
});
(action.fields || []).forEach(function (fieldConfig) {
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
return node.getAttribute('name') === fieldConfig.name;
});
if (!input) return;
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
if (raw === '' && !input.checked && !fieldConfig.required) return;
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
});
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
if (action.confirmArg) args.confirm = true;
return args;
}
function setStatus(message, error) {
elements.status.textContent = message || '';
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
}
function submitAction() {
if (state.busy || !state.action) return;
var action = state.action;
var args;
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
state.busy = true;
elements.submit.disabled = true;
elements.cancel.disabled = true;
setStatus(t('loading'), false);
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
unwrap(result);
setStatus(t('success'), false);
var refresh = document.getElementById('refresh');
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
}).catch(function (error) {
setStatus(error && error.message ? error.message : t('failure'), true);
}).finally(function () {
state.busy = false;
elements.submit.disabled = false;
elements.cancel.disabled = false;
});
}
function recordFromNode(node) {
var content = String(node.textContent || '').toLowerCase();
var exact = state.records.find(function (item) {
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
return id && content.indexOf(id) !== -1;
});
if (exact) return exact;
return state.records.find(function (item) {
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
return title && content.indexOf(title) !== -1;
}) || null;
}
function update(value) {
var payload = normalize(value) || {};
var widget = widgetFrom(payload);
if (Object.keys(widget).length) state.widget = widget;
var records = recordsFrom(payload);
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
ensurePanel();
var actions = actionList();
elements.panel.hidden = actions.length === 0;
if (actions.length && !state.action) renderForm();
else if (actions.length) renderForm();
}
document.addEventListener('click', function (event) {
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
if (!node || (elements.panel && elements.panel.contains(node))) return;
var item = recordFromNode(node);
if (!item) return;
state.selected = item;
ensurePanel();
renderForm();
}, true);
window.addEventListener('message', function (event) {
if (event.source !== window.parent) return;
var message = event.data;
if (!message || message.jsonrpc !== '2.0') return;
if (message.id !== undefined && pending.has(message.id)) {
var requestState = pending.get(message.id);
pending.delete(message.id);
window.clearTimeout(requestState.timeout);
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
else requestState.resolve(message.result);
return;
}
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
}, { passive: true });
ensurePanel();
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
if (initial) update(initial);
}());
</script>
</body>
</html>
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"name": "shd-acl-matrix",
"version": "1.0.0",
"resources": [
{
"file": "index.html",
"bytes": 69107,
"gzipBytes": 18847,
"mediaType": "text/html;profile=mcp-app",
"uri": "ui://shd/acl-matrix/v1.html",
"sha256": "818956bcece3e1a64a8f41f51a8bcc19ebcd0c95d1eb9bfa923166dca9660711"
}
],
"license": "../../../../../LICENSE",
"source": {
"repository": "https://github.com/bulava92/shd-mcp-plugin",
"path": "plugins/shd-mcp-plugin/widgets/acl-matrix/v1/index.html"
}
}
@@ -0,0 +1,3 @@
See the repository root LICENSE: the SHD MCP Plugin License applies to this
widget.
This file is a local provenance notice; it does not grant additional rights.
@@ -0,0 +1 @@
7c0ad632ddc013afd70ce80789a73c3b99aa3030d9d7b1d440ba1902aadae42d index.html
@@ -0,0 +1,12 @@
# SHD active-projects widget provenance
This is an SHD-authored MCP Apps resource. It is not copied from an external
widget bundle and has no OpenAI upstream dependency.
- Resource URI: `ui://shd/active-projects/v1.html`
- Media type: `text/html;profile=mcp-app`
- Source: `plugins/shd-mcp-plugin/widgets/active-projects/v1/index.html`
- Backend loader: `SHD_MCP_WIDGET_ROOT` or the repository path documented in
`scripts/mcp/mcp/resources/active-projects-widget.mjs`
- Integrity: verify `index.html` against `SHA256SUMS`.
- License: repository root `LICENSE`.
@@ -0,0 +1,949 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
color-scheme: light dark;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--bg: #f8fafc;
--surface: #e8edf2;
--surface-hover: #e1e8ef;
--border: rgba(31, 41, 55, .16);
--text: rgb(29, 31, 37);
--muted: rgb(97, 102, 112);
--accent: rgb(83, 89, 101);
--accent-soft: rgba(75, 85, 99, .12);
--success: #16794b;
--success-soft: #e6f7ef;
--warning: #9a6100;
--warning-soft: #fff4d8;
--danger: #b42318;
--danger-soft: #ffebe9;
--shadow: 0 0 0 1px rgba(15, 23, 42, .07), 0 8px 22px rgba(15, 23, 42, .11);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #202020;
--surface: #292929;
--surface-hover: #303336;
--border: #43474c;
--text: #eaecf0;
--muted: #96999d;
--accent: #c7b07a;
--accent-soft: rgba(199, 176, 122, .18);
--success: #70d4a4;
--success-soft: #173d31;
--warning: #f4c467;
--warning-soft: #4b3918;
--danger: #ff9b91;
--danger-soft: #512522;
--shadow: 0 10px 26px rgba(0, 0, 0, .34);
}
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--text); }
button, input { font: inherit; }
button { color: inherit; }
.app { padding: 16px; max-width: 900px; margin: 0 auto; }
.header { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; }
h1 { margin: 0; font-size: 20px; line-height: 1.25; letter-spacing: -.01em; }
.subtitle { margin: 5px 0 0; color: var(--muted); font-size: 13px; }
.toolbar { display: flex; align-items: center; gap: 8px; margin-top: 16px; }
.search { flex: 1; min-width: 0; border: 1px solid var(--border); border-radius: 9px; padding: 9px 11px; background: var(--bg); color: var(--text); outline: none; }
.search:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.button { border: 1px solid var(--border); border-radius: 9px; padding: 8px 11px; background: var(--surface); cursor: pointer; white-space: nowrap; }
.button:hover { background: var(--surface-hover); }
.button:focus-visible, .project-row:focus-visible { outline: 3px solid var(--accent-soft); outline-offset: 2px; }
.button:disabled { cursor: wait; opacity: .62; }
.summary { display: flex; flex-wrap: wrap; gap: 8px; margin: 14px 0; }
.summary-item { border-radius: 999px; padding: 5px 9px; background: var(--surface); color: var(--muted); font-size: 12px; }
.summary-item strong { color: var(--text); font-weight: 650; }
.table { border: 1px solid var(--border); border-radius: 11px; overflow: hidden; box-shadow: var(--shadow); }
.table-head, .project-row { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(130px, .8fr) minmax(110px, .7fr); gap: 12px; align-items: center; }
.table-head { padding: 9px 12px; background: var(--surface); color: var(--muted); font-size: 11px; font-weight: 650; text-transform: uppercase; letter-spacing: .04em; }
.project-row { width: 100%; border: 0; border-top: 1px solid var(--border); padding: 12px; background: var(--bg); text-align: left; cursor: pointer; }
.project-row:hover { background: var(--surface-hover); }
.project-name { min-width: 0; }
.project-title { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 620; }
.project-code { display: block; margin-top: 3px; color: var(--muted); font-size: 12px; }
.deadline { color: var(--text); font-size: 13px; }
.deadline.missing { color: var(--muted); }
.deadline.overdue { color: var(--danger); font-weight: 620; }
.status { display: inline-flex; width: fit-content; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-radius: 999px; padding: 5px 9px; font-size: 12px; font-weight: 620; }
.status-neutral { background: var(--surface); color: var(--muted); }
.status-progress { background: var(--accent-soft); color: var(--accent); }
.status-done { background: var(--success-soft); color: var(--success); }
.status-warning { background: var(--warning-soft); color: var(--warning); }
.status-danger { background: var(--danger-soft); color: var(--danger); }
.empty, .error { padding: 22px 14px; color: var(--muted); text-align: center; }
.error { color: var(--danger); }
.details { margin-top: 14px; padding: 14px; border: 1px solid var(--border); border-radius: 11px; background: var(--surface); }
.details[hidden], .error[hidden] { display: none; }
.details-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.details-title { margin: 0; font-size: 16px; }
.details-code { margin: 3px 0 0; color: var(--muted); font-size: 12px; }
.details-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin: 14px 0 0; }
.detail-label { display: block; margin-bottom: 3px; color: var(--muted); font-size: 11px; }
.detail-value { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
.loading { color: var(--muted); font-size: 13px; }
@media (max-width: 600px) {
.app { padding: 12px; }
.header { display: block; }
.header .button { margin-top: 10px; }
.table-head, .project-row { grid-template-columns: minmax(0, 1.4fr) minmax(100px, .8fr) minmax(90px, .7fr); gap: 8px; }
.table-head, .project-row { padding-left: 9px; padding-right: 9px; }
.details-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
</style>
<style data-shd-widget-write-actions="css">
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
.shd-widget-actions[hidden]{display:none}
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
.shd-widget-actions-field:has(textarea){grid-column:span 2}
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
.shd-widget-actions-danger{color:var(--danger,#b42318)}
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
</style>
</head>
<body>
<main class="app" aria-live="polite">
<header class="header">
<div>
<h1 data-i18n="title">Активные проекты</h1>
<p class="subtitle" data-i18n="subtitle">Сроки завершения и текущие статусы</p>
</div>
<button class="button" id="refresh" type="button" data-i18n="refresh">Обновить</button>
</header>
<div class="toolbar">
<input class="search" id="search" type="search" autocomplete="off" data-i18n-placeholder="search" placeholder="Поиск по проекту или коду">
</div>
<div class="summary" id="summary"></div>
<section class="table" aria-label="Проекты">
<div class="table-head">
<span data-i18n="project">Проект</span>
<span data-i18n="deadline">Дата завершения</span>
<span data-i18n="status">Статус</span>
</div>
<div id="rows"></div>
<div class="empty" id="empty" hidden></div>
<div class="error" id="error" hidden></div>
</section>
<section class="details" id="details" hidden>
<div class="details-header">
<div>
<h2 class="details-title" id="details-title"></h2>
<p class="details-code" id="details-code"></p>
</div>
<button class="button" id="close-details" type="button" data-i18n="close">Закрыть</button>
</div>
<div class="details-grid" id="details-grid"></div>
<p class="loading" id="details-loading" hidden></p>
</section>
</main>
<script>
(function () {
'use strict';
var messages = {
ru: {
title: 'Активные проекты',
subtitle: 'Сроки завершения и текущие статусы',
refresh: 'Обновить',
search: 'Поиск по проекту или коду',
project: 'Проект',
deadline: 'Дата завершения',
status: 'Статус',
close: 'Закрыть',
missingDate: 'Не указана',
noProjects: 'Проекты не найдены.',
noMatch: 'По фильтру ничего не найдено.',
loading: 'Загрузка…',
loadingProject: 'Загрузка данных проекта…',
refreshError: 'Не удалось обновить список проектов.',
projectError: 'Не удалось получить данные проекта.',
total: 'Всего',
withoutDate: 'Без даты',
archived: 'Архивных',
phase: 'Этап',
timezone: 'Часовой пояс',
updated: 'Обновлён',
code: 'Код'
},
en: {
title: 'Active projects',
subtitle: 'Completion dates and current statuses',
refresh: 'Refresh',
search: 'Search by project or code',
project: 'Project',
deadline: 'Completion date',
status: 'Status',
close: 'Close',
missingDate: 'Not set',
noProjects: 'No projects found.',
noMatch: 'No projects match the filter.',
loading: 'Loading…',
loadingProject: 'Loading project data…',
refreshError: 'Could not refresh the project list.',
projectError: 'Could not load the project data.',
total: 'Total',
withoutDate: 'No date',
archived: 'Archived',
phase: 'Phase',
timezone: 'Timezone',
updated: 'Updated',
code: 'Code'
}
};
var locale = (document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en';
var t = messages[locale];
var currentSnapshot = null;
var currentProjects = [];
var latestToolInput = null;
var pendingRequests = new Map();
var nextRequestId = 1;
var mcpAppsProtocolVersion = '2026-01-26';
var busy = false;
var elements = {
refresh: document.getElementById('refresh'),
search: document.getElementById('search'),
summary: document.getElementById('summary'),
rows: document.getElementById('rows'),
empty: document.getElementById('empty'),
error: document.getElementById('error'),
details: document.getElementById('details'),
detailsTitle: document.getElementById('details-title'),
detailsCode: document.getElementById('details-code'),
detailsGrid: document.getElementById('details-grid'),
detailsLoading: document.getElementById('details-loading'),
closeDetails: document.getElementById('close-details')
};
function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(function (node) {
var key = node.getAttribute('data-i18n');
if (t[key]) node.textContent = t[key];
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(function (node) {
var key = node.getAttribute('data-i18n-placeholder');
if (t[key]) node.setAttribute('placeholder', t[key]);
});
}
function request(method, params) {
var openai = typeof window !== 'undefined' ? window.openai : undefined;
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
return openai.callTool(params.name, params.arguments || {});
}
var id = nextRequestId++;
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params }, '*');
return new Promise(function (resolve, reject) {
var timeout = window.setTimeout(function () {
pendingRequests.delete(id);
reject(new Error('Host request timed out'));
}, 10000);
pendingRequests.set(id, { resolve: resolve, reject: reject, timeout: timeout });
});
}
function notify(method, params) {
window.parent.postMessage({ jsonrpc: '2.0', method: method, params: params || {} }, '*');
}
function initializeHost() {
return request('ui/initialize', {
protocolVersion: mcpAppsProtocolVersion,
appInfo: {
name: 'SHD Active Projects',
title: 'SHD Active Projects',
version: '1.0.0',
websiteUrl: 'https://shd.xyz.su'
},
appCapabilities: {}
}).then(function () {
notify('ui/notifications/initialized', {});
}).catch(function () {
// The window.openai compatibility bridge can work without the raw postMessage handshake.
});
}
function normalizePayload(value) {
if (!value) return null;
if (value.structuredContent) return value.structuredContent;
if (value.toolOutput) return value.toolOutput;
return value;
}
function getProjects(value) {
var payload = normalizePayload(value);
if (!payload || payload.error) return [];
if (Array.isArray(payload.projects)) return payload.projects.filter(function (item) { return item && typeof item === 'object'; });
var data = payload.data;
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
return Array.isArray(data) ? data.filter(function (item) { return item && typeof item === 'object'; }) : [];
}
function getProjectCode(project) {
return String(project.code || project.project_code || project.contract_project_id || project.id || '');
}
function getProjectName(project) {
return String(project.display_name || project.title || project.name || getProjectCode(project) || '—');
}
function getStatus(project) {
return String(project.status || project.phase || '—');
}
function getTone(status) {
var value = String(status).toLowerCase();
if (/done|complete|completed|closed|resolved|finished|готов|заверш/.test(value)) return 'done';
if (/cancel|reject|blocked|error|отмен|отклон|блок/.test(value)) return 'danger';
if (/progress|active|work|processing|в работе|актив/.test(value)) return 'progress';
if (/pending|review|pause|ожид|провер|пауза/.test(value)) return 'warning';
return 'neutral';
}
function getDeadlineTime(project) {
var value = project.deadline;
if (!value) return Number.POSITIVE_INFINITY;
var timestamp = Date.parse(String(value));
return Number.isNaN(timestamp) ? Number.POSITIVE_INFINITY : timestamp;
}
function formatDate(value) {
if (!value) return t.missingDate;
var timestamp = Date.parse(String(value));
if (Number.isNaN(timestamp)) return String(value);
try {
return new Intl.DateTimeFormat(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }).format(new Date(timestamp));
} catch (error) {
return String(value);
}
}
function isOverdue(project) {
var timestamp = getDeadlineTime(project);
var status = getStatus(project).toLowerCase();
return Number.isFinite(timestamp) && timestamp < Date.now() && !/done|complete|closed|resolved|заверш/.test(status);
}
function makeElement(tag, text, className) {
var node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function renderSummary(projects) {
elements.summary.replaceChildren();
var noDate = projects.filter(function (project) { return !project.deadline; }).length;
var archived = projects.filter(function (project) { return project.is_archived === true; }).length;
var total = makeElement('span', '', 'summary-item');
total.append(makeElement('strong', String(projects.length)), ' ' + t.total);
elements.summary.append(total);
if (noDate) {
var missing = makeElement('span', '', 'summary-item');
missing.append(makeElement('strong', String(noDate)), ' ' + t.withoutDate);
elements.summary.append(missing);
}
if (archived) {
var archivedNode = makeElement('span', '', 'summary-item');
archivedNode.append(makeElement('strong', String(archived)), ' ' + t.archived);
elements.summary.append(archivedNode);
}
}
function renderRows() {
var query = elements.search.value.trim().toLowerCase();
var projects = currentProjects.filter(function (project) {
if (!query) return true;
return (getProjectName(project) + ' ' + getProjectCode(project) + ' ' + getStatus(project)).toLowerCase().indexOf(query) !== -1;
});
elements.rows.replaceChildren();
elements.error.hidden = true;
elements.empty.hidden = projects.length !== 0;
elements.empty.textContent = currentProjects.length === 0 ? t.noProjects : t.noMatch;
projects.forEach(function (project) {
var row = makeElement('button', '', 'project-row');
row.type = 'button';
row.setAttribute('aria-label', getProjectName(project));
var name = makeElement('span', '', 'project-name');
name.append(makeElement('span', getProjectName(project), 'project-title'));
name.append(makeElement('span', getProjectCode(project), 'project-code'));
var deadline = makeElement('span', formatDate(project.deadline), 'deadline' + (project.deadline ? '' : ' missing') + (isOverdue(project) ? ' overdue' : ''));
var status = makeElement('span', getStatus(project), 'status status-' + getTone(getStatus(project)));
row.append(name, deadline, status);
row.addEventListener('click', function () { showProject(project); });
elements.rows.append(row);
});
}
function render(value) {
var payload = normalizePayload(value);
currentSnapshot = payload;
currentProjects = getProjects(payload).slice().sort(function (left, right) {
return getDeadlineTime(left) - getDeadlineTime(right) || getProjectName(left).localeCompare(getProjectName(right), locale);
});
renderSummary(currentProjects);
renderRows();
}
function setBusy(nextBusy, label) {
busy = nextBusy;
elements.refresh.disabled = nextBusy;
if (nextBusy) elements.refresh.textContent = label || t.loading;
else elements.refresh.textContent = t.refresh;
}
function setError(message) {
elements.error.hidden = false;
elements.error.textContent = message;
}
function unwrapResult(value) {
var payload = normalizePayload(value);
if (value && value.isError) throw new Error(payload && payload.error ? payload.error : t.refreshError);
if (payload && payload.error) throw new Error(String(payload.error));
return payload || value;
}
async function refresh() {
if (busy) return;
setBusy(true);
elements.error.hidden = true;
try {
var result = await request('tools/call', { name: 'shd_list_projects', arguments: { archived: false, limit: 100 } });
render(unwrapResult(result));
} catch (error) {
setError(error && error.message ? error.message : t.refreshError);
} finally {
setBusy(false);
}
}
function addDetail(label, value) {
var item = makeElement('div');
item.append(makeElement('span', label, 'detail-label'), makeElement('span', value || '—', 'detail-value'));
elements.detailsGrid.append(item);
}
function showDetailSnapshot(project) {
elements.detailsTitle.textContent = getProjectName(project);
elements.detailsCode.textContent = getProjectCode(project) ? t.code + ': ' + getProjectCode(project) : '';
elements.detailsGrid.replaceChildren();
addDetail(t.deadline, formatDate(project.deadline));
addDetail(t.status, getStatus(project));
addDetail(t.phase, String(project.phase || '—'));
addDetail(t.timezone, String(project.timezone || '—'));
addDetail(t.updated, formatDate(project.updated_at));
elements.details.hidden = false;
}
async function showProject(project) {
showDetailSnapshot(project);
var code = getProjectCode(project);
if (!code) return;
elements.detailsLoading.hidden = false;
elements.detailsLoading.textContent = t.loadingProject;
try {
var result = await request('tools/call', { name: 'shd_get_project', arguments: { project_code: code } });
var payload = unwrapResult(result);
var data = payload && payload.data;
if (Array.isArray(data)) data = data[0];
if (data && typeof data === 'object') showDetailSnapshot(data);
} catch (error) {
setError(error && error.message ? error.message : t.projectError);
} finally {
elements.detailsLoading.hidden = true;
}
}
window.addEventListener('message', function (event) {
if (event.source !== window.parent) return;
var message = event.data;
if (!message || message.jsonrpc !== '2.0') return;
if (message.id !== undefined && pendingRequests.has(message.id)) {
var pending = pendingRequests.get(message.id);
pendingRequests.delete(message.id);
window.clearTimeout(pending.timeout);
if (message.error) pending.reject(new Error(message.error.message || t.refreshError));
else pending.resolve(message.result);
return;
}
if (message.method === 'ui/notifications/tool-input') {
latestToolInput = message.params || null;
if (!currentProjects.length) render(latestToolInput);
}
if (message.method === 'ui/notifications/tool-result') render(message.params && message.params.structuredContent);
}, { passive: true });
elements.search.addEventListener('input', renderRows);
elements.refresh.addEventListener('click', refresh);
elements.closeDetails.addEventListener('click', function () { elements.details.hidden = true; });
applyTranslations();
void initializeHost();
var initialOutput = typeof window !== 'undefined' && window.openai ? window.openai.toolOutput : null;
if (initialOutput) render(initialOutput);
else {
latestToolInput = typeof window !== 'undefined' && window.openai ? window.openai.toolInput : null;
render(latestToolInput || currentSnapshot);
}
}());
</script>
<script data-shd-widget-write-actions="js">
(function () {
'use strict';
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
var copy = {
ru: {
actions: 'Действия',
chooseAction: 'Выберите действие',
selectRecord: 'Сначала выберите запись.',
submit: 'Сохранить',
cancel: 'Отмена',
confirm: 'Подтверждаю действие',
warning: 'Проверьте данные перед записью.',
destructive: 'Операция может изменить или удалить данные.',
required: 'Заполните обязательное поле.',
invalidJson: 'Введите корректный JSON.',
success: 'Изменения сохранены.',
failure: 'Не удалось сохранить изменения.',
loading: 'Сохраняю…',
noActions: 'Для этого виджета нет доступных действий.',
refreshed: 'Список обновлён.',
fieldValue: 'Значение',
},
en: {
actions: 'Actions',
chooseAction: 'Choose an action',
selectRecord: 'Select a record first.',
submit: 'Save',
cancel: 'Cancel',
confirm: 'I confirm this action',
warning: 'Review the values before writing.',
destructive: 'This operation may change or remove data.',
required: 'Fill in the required field.',
invalidJson: 'Enter valid JSON.',
success: 'Changes saved.',
failure: 'Could not save changes.',
loading: 'Saving…',
noActions: 'No actions are available for this widget.',
refreshed: 'List refreshed.',
fieldValue: 'Value',
},
};
var state = {
widget: {},
records: [],
selected: null,
action: null,
inputs: {},
busy: false,
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
};
var pending = new Map();
var nextRequestId = 1;
var elements = {};
function t(key) {
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
}
function object(value) {
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}
function normalize(value) {
if (!value) return null;
if (value.structuredContent) return normalize(value.structuredContent);
if (value.toolOutput) return normalize(value.toolOutput);
if (value.toolInput) return normalize(value.toolInput);
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
return value;
}
function widgetFrom(value) {
var payload = normalize(value) || {};
var meta = object(payload.meta) ? payload.meta : {};
return object(meta.widget) ? meta.widget : {};
}
function recordsFrom(value) {
var payload = normalize(value) || {};
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
if (!Array.isArray(data)) data = payload.data;
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
}
function readPath(value, path) {
if (!value || path === undefined || path === null) return undefined;
var parts = String(path).split('.');
var current = value;
for (var index = 0; index < parts.length; index += 1) {
if (current === undefined || current === null) return undefined;
current = current[parts[index]];
}
return current;
}
function firstValue(paths) {
var list = Array.isArray(paths) ? paths : [paths];
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
var sourceObject = sources[sourceIndex];
if (!sourceObject) continue;
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
var value = readPath(sourceObject, list[pathIndex]);
if (value !== undefined && value !== null && value !== '') return value;
}
}
return undefined;
}
function text(value) {
if (value === undefined || value === null) return '';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
}
function label(value) {
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
return String(value || t('fieldValue'));
}
function actionLabel(action) {
return label(action && action.label) || action.id || action.tool || t('actions');
}
function hasTargets(action) {
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
}
function actionList() {
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
return action && action.tool;
}) : [];
}
function request(method, params) {
var openai = typeof window !== 'undefined' ? window.openai : undefined;
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
return openai.callTool(params.name, params.arguments || {});
}
var id = nextRequestId++;
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
return new Promise(function (resolve, reject) {
var timeout = window.setTimeout(function () {
pending.delete(id);
reject(new Error(t('failure')));
}, 15000);
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
});
}
function unwrap(value) {
var payload = normalize(value);
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
if (payload && payload.error) throw new Error(text(payload.error));
return payload || value;
}
function make(tag, value, className) {
var node = document.createElement(tag);
if (className) node.className = className;
if (value !== undefined) node.textContent = value;
return node;
}
function ensurePanel() {
if (elements.panel) return;
var root = document.querySelector('main') || document.body;
var panel = make('section', undefined, 'shd-widget-actions');
panel.hidden = true;
panel.setAttribute('aria-live', 'polite');
var header = make('div', undefined, 'shd-widget-actions-header');
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
var select = document.createElement('select');
select.className = 'shd-widget-actions-select';
select.setAttribute('aria-label', t('actions'));
var form = make('form', undefined, 'shd-widget-actions-form');
var status = make('div', '', 'shd-widget-actions-status');
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
submit.type = 'submit';
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
cancel.type = 'button';
buttons.append(submit, cancel);
panel.append(header, select, form, status, buttons);
root.append(panel);
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
select.addEventListener('change', function () {
var selected = actionList().find(function (action) { return action.id === select.value; });
state.action = selected || null;
renderForm();
});
cancel.addEventListener('click', function () {
state.action = null;
select.value = '';
renderForm();
});
form.addEventListener('submit', function (event) {
event.preventDefault();
submitAction();
});
}
function inputValue(fieldConfig) {
var value = firstValue(fieldConfig.source || []);
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
return value;
}
function createInput(fieldConfig) {
var type = fieldConfig.type || 'text';
var input;
if (type === 'textarea' || type === 'json') {
input = document.createElement('textarea');
input.rows = type === 'json' ? 4 : 3;
if (type === 'json') input.placeholder = '{ }';
} else if (type === 'select') {
input = document.createElement('select');
(fieldConfig.options || []).forEach(function (option) {
var item = document.createElement('option');
item.value = String(option[0]);
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
input.append(item);
});
} else {
input = document.createElement('input');
input.type = type === 'number' || type === 'email' ? type : 'text';
}
input.className = 'shd-widget-actions-input';
input.name = fieldConfig.name;
var value = inputValue(fieldConfig);
if (type === 'checkbox') {
input.type = 'checkbox';
input.checked = value === true || value === 'true' || value === 1 || value === '1';
} else if (value !== undefined && value !== null) {
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
}
if (fieldConfig.required) input.required = true;
return input;
}
function renderForm() {
ensurePanel();
var selectedActionId = state.action ? state.action.id : '';
elements.select.replaceChildren();
var placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = t('chooseAction');
elements.select.append(placeholder);
actionList().forEach(function (action) {
var option = document.createElement('option');
option.value = action.id;
option.textContent = actionLabel(action);
elements.select.append(option);
});
elements.select.value = selectedActionId;
elements.select.hidden = actionList().length === 0;
elements.form.replaceChildren();
elements.status.textContent = '';
elements.submit.disabled = state.busy;
var action = state.action;
if (!action) {
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
elements.submit.hidden = true;
elements.cancel.hidden = true;
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
}
return;
}
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
elements.submit.hidden = false;
elements.cancel.hidden = false;
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
(action.fields || []).forEach(function (fieldConfig) {
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
var wrapper = make('label', undefined, 'shd-widget-actions-field');
wrapper.append(make('span', label(fieldConfig.label)));
wrapper.append(createInput(fieldConfig));
elements.form.append(wrapper);
});
if (action.confirm) {
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
var confirm = document.createElement('input');
confirm.type = 'checkbox';
confirm.name = '__confirm';
confirm.required = true;
confirmLabel.append(confirm, make('span', t('confirm')));
elements.form.append(confirmLabel);
}
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
elements.form.append(notice);
}
function coerce(value, type) {
if (type === 'number') {
var number = Number(value);
return Number.isFinite(number) ? number : value;
}
if (type === 'checkbox') return Boolean(value);
if (type === 'json') {
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
}
return value;
}
function targetType(name) {
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
}
function targetList(action) {
if (Array.isArray(action.targets)) return action.targets;
return action.target ? [action.target] : [];
}
function collectArguments(action) {
var args = Object.assign({}, action.fixed || {});
(action.context || []).forEach(function (name) {
var value = firstValue([name]);
if (value !== undefined && value !== null && value !== '') args[name] = value;
});
targetList(action).forEach(function (targetConfig) {
var value = firstValue(targetConfig.source || targetConfig.name);
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
});
(action.fields || []).forEach(function (fieldConfig) {
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
return node.getAttribute('name') === fieldConfig.name;
});
if (!input) return;
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
if (raw === '' && !input.checked && !fieldConfig.required) return;
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
});
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
if (action.confirmArg) args.confirm = true;
return args;
}
function setStatus(message, error) {
elements.status.textContent = message || '';
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
}
function submitAction() {
if (state.busy || !state.action) return;
var action = state.action;
var args;
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
state.busy = true;
elements.submit.disabled = true;
elements.cancel.disabled = true;
setStatus(t('loading'), false);
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
unwrap(result);
setStatus(t('success'), false);
var refresh = document.getElementById('refresh');
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
}).catch(function (error) {
setStatus(error && error.message ? error.message : t('failure'), true);
}).finally(function () {
state.busy = false;
elements.submit.disabled = false;
elements.cancel.disabled = false;
});
}
function recordFromNode(node) {
var content = String(node.textContent || '').toLowerCase();
var exact = state.records.find(function (item) {
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
return id && content.indexOf(id) !== -1;
});
if (exact) return exact;
return state.records.find(function (item) {
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
return title && content.indexOf(title) !== -1;
}) || null;
}
function update(value) {
var payload = normalize(value) || {};
var widget = widgetFrom(payload);
if (Object.keys(widget).length) state.widget = widget;
var records = recordsFrom(payload);
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
ensurePanel();
var actions = actionList();
elements.panel.hidden = actions.length === 0;
if (actions.length && !state.action) renderForm();
else if (actions.length) renderForm();
}
document.addEventListener('click', function (event) {
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
if (!node || (elements.panel && elements.panel.contains(node))) return;
var item = recordFromNode(node);
if (!item) return;
state.selected = item;
ensurePanel();
renderForm();
}, true);
window.addEventListener('message', function (event) {
if (event.source !== window.parent) return;
var message = event.data;
if (!message || message.jsonrpc !== '2.0') return;
if (message.id !== undefined && pending.has(message.id)) {
var requestState = pending.get(message.id);
pending.delete(message.id);
window.clearTimeout(requestState.timeout);
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
else requestState.resolve(message.result);
return;
}
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
}, { passive: true });
ensurePanel();
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
if (initial) update(initial);
}());
</script>
</body>
</html>
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"name": "shd-active-projects",
"version": "1.0.0",
"resources": [
{
"file": "index.html",
"bytes": 43172,
"gzipBytes": 10825,
"mediaType": "text/html;profile=mcp-app",
"uri": "ui://shd/active-projects/v1.html",
"sha256": "7c0ad632ddc013afd70ce80789a73c3b99aa3030d9d7b1d440ba1902aadae42d"
}
],
"license": "../../../../../LICENSE",
"source": {
"repository": "https://github.com/bulava92/shd-mcp-plugin",
"path": "plugins/shd-mcp-plugin/widgets/active-projects/v1/index.html"
}
}
@@ -0,0 +1 @@
766b830b207f2929c16635f24081bf11e9e69f2c9a4cb261157a79a6cfd4dc63 index.html
@@ -0,0 +1,5 @@
# Activity widget
This artifact is served by the SHD MCP backend as ui://shd/activity/v1.html.
The HTML is standalone, uses the MCP Apps bridge first, and keeps window.openai as a compatibility extension. It has no external script or network dependency. The widget provides Authenticated realtime activity stream.
@@ -0,0 +1,560 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SHD MCP</title>
<style>
:root{color-scheme:light dark;--bg:#f8fafc;--surface:#e8edf2;--hover:#e1e8ef;--line:rgba(31,41,55,.16);--ink:#1d1f25;--muted:#616670;--accent:#535963;--soft:rgba(75,85,99,.12);--bad:#b42318;--good:#2f855a;--button:#fff;--shadow:0 10px 26px rgba(31,41,55,.1)}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.shell{max-width:980px;margin:auto;padding:22px}.card{padding:20px;border:1px solid var(--line);border-radius:18px;background:var(--surface);box-shadow:var(--shadow)}
.eyebrow{color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}h1{margin:4px 0 5px;font-size:25px;line-height:1.15}.lede{margin:0;color:var(--muted)}.summary{display:flex;gap:8px;flex-wrap:wrap;margin:18px 0 14px}.summary span{padding:5px 9px;border:1px solid var(--line);border-radius:999px;background:var(--hover);color:var(--muted)}.summary strong{color:var(--ink)}
.toolbar{display:flex;gap:8px;margin-bottom:10px}.search{min-width:0;flex:1;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:inherit;outline:0}.search:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--soft)}button{cursor:pointer}.refresh{padding:10px 14px;border:1px solid var(--accent);border-radius:10px;background:var(--accent);color:var(--button);font-weight:650}.refresh:disabled{opacity:.6;cursor:wait}.status{min-height:22px;color:var(--muted)}.error{color:var(--bad)}
.table-wrap{max-height:460px;overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--bg)}table{width:100%;border-collapse:collapse;min-width:560px}th,td{padding:11px 13px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}th{position:sticky;top:0;background:var(--hover);color:var(--muted);font-size:12px;font-weight:650;z-index:1}tbody tr:last-child td{border-bottom:0}.record{cursor:pointer;outline:0}.record:hover,.record:focus{background:var(--soft)}.title{font-weight:650}.sub{display:block;color:var(--muted);font-size:12px;margin-top:2px}.pill{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--soft);color:var(--accent);font-size:12px;white-space:nowrap}.pill.up,.pill.ok,.pill.active{color:var(--good);background:rgba(47,133,90,.14)}.pill.down,.pill.error,.pill.overdue{color:var(--bad);background:rgba(180,35,24,.12)}.empty{padding:24px;text-align:center;color:var(--muted)}
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
</style>
<style data-shd-widget-write-actions="css">
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
.shd-widget-actions[hidden]{display:none}
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
.shd-widget-actions-field:has(textarea){grid-column:span 2}
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
.shd-widget-actions-danger{color:var(--danger,#b42318)}
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
</style>
</head>
<body>
<main class="shell" data-widget-module="notes"><section class="card">
<div class="eyebrow">SHD MCP</div><h1 id="title">SHD</h1><p class="lede" id="desc"></p><div class="summary" id="summary"></div>
<div class="toolbar"><input class="search" id="search" type="search" autocomplete="off"><button class="refresh" id="refresh" type="button"></button></div>
<div class="status" id="status" role="status" aria-live="polite"></div>
<div class="table-wrap"><table><thead><tr><th id="head-title"></th><th id="head-status"></th><th id="head-date"></th><th id="head-value"></th></tr></thead><tbody id="rows"></tbody></table><div class="empty" id="empty" hidden></div></div>
<section class="details" id="details" hidden aria-live="polite"><div class="detail-head"><div><h2 id="detail-title"></h2><div class="sub" id="detail-sub"></div></div><button class="close" id="close" type="button">×</button></div><div class="grid" id="grid"></div></section>
</section></main>
<script>
(function(){
'use strict';
var DEFAULT_MODULE='activity';
var C={ru:{refresh:'Обновить',search:'Поиск по названию, статусу или ID',loading:'Загрузка…',empty:'Нет данных для отображения.',records:'записей',status:'Статус',date:'Дата',value:'Детали',id:'ID',details:'Детали',close:'Закрыть',load:'Не удалось загрузить данные.',detail:'Не удалось загрузить детали.',context:'Для обновления нужен контекст проекта или базы'},en:{refresh:'Refresh',search:'Search by name, status or ID',loading:'Loading…',empty:'No data to display.',records:'records',status:'Status',date:'Date',value:'Details',id:'ID',details:'Details',close:'Close',load:'Could not load data.',detail:'Could not load details.',context:'A project or base context is required to refresh.'}};
var M={
discussions:{ru:['Обсуждения','Темы обсуждений с поиском и просмотром сообщений.','Тема','Обновлено','Сообщение'],en:['Discussions','Discussion topics with search and message details.','Topic','Updated','Message']},
notifications:{ru:['Уведомления','Последние уведомления и события модулей.','Уведомление','Время','Источник'],en:['Notifications','Recent notifications and module events.','Notification','Time','Source']},
scheduling:{ru:['Расписание','Ближайшие события и записи из расписания.','Событие','Начало','Участник'],en:['Scheduling','Upcoming events and bookings from scheduling.','Event','Starts','Guest']},
inventory:{ru:['Склад','Складские позиции, остатки и статусы.','Позиция','Обновлено','Количество'],en:['Inventory','Stock items, quantities and statuses.','Item','Updated','Quantity']},
agents:{ru:['Агенты и контроллеры','Доступные агенты, контроллеры и их текущее состояние.','Агент или контроллер','Проверено','Тип'],en:['Agents & Controllers','Available agents, controllers and their current state.','Agent or controller','Checked','Type']},
'status-page':{ru:['Status Page','Состояние доступности сервисов и мониторов.','Монитор','Проверено','Доступность'],en:['Status Page','Service availability and monitor state.','Monitor','Checked','Availability']},
'project-db':{ru:['Project DB','Каталог таблиц Project DB с просмотром схемы.','Таблица','Обновлено','Записей'],en:['Project DB','Project DB table catalog with schema details.','Table','Updated','Records']},
notes:{ru:['Заметки','Страницы Notes с поиском и просмотром содержимого.','Страница','Обновлено','Пространство'],en:['Notes','Notes pages with search and content details.','Page','Updated','Space']},
'project-overview':{ru:['Обзор проекта','Сводные данные по разделам выбранного проекта.','Раздел','Обновлено','Элементы'],en:['Project overview','Cross-module records for the selected project.','Section','Updated','Items']},
files:{ru:['Файлы','Файлы выбранного проекта с поиском и безопасным просмотром ссылок.','Файл','Обновлено','Размер или тип'],en:['Files','Files from the selected project with search and safe links.','File','Updated','Size or type']},
proposals:{ru:['Предложения','Предложения и сметы с поиском, статусами и деталями.','Предложение','Обновлено','Сумма или номер'],en:['Proposals','Proposals with search, statuses and details.','Proposal','Updated','Amount or number']},
terms:{ru:['Terms / ТЗ','Документы технического задания и их ревизии.','Документ','Обновлено','Страницы или ревизия'],en:['Terms documents','Terms of Reference documents and revisions.','Document','Updated','Pages or revision']},
activity:{ru:['Активность','Авторизованные realtime-события SHD по выбранному scope.','Событие','Время','Модуль или тип'],en:['Activity','Authenticated SHD realtime events for the selected scope.','Event','Time','Module or type']},
organizations:{ru:['Организации и доступ','Организации, роли и безопасный просмотр участников.','Организация','Обновлено','Роль или тип'],en:['Organizations & access','Organizations, roles and safe membership details.','Organization','Updated','Role or type']},
gitea:{ru:['Gitea Issues','Открытые задачи разрешённого site или project репозитория.','Issue','Обновлено','Состояние'],en:['Gitea Issues','Open issues for an allowed site or project repository.','Issue','Updated','State']},
schemes:{ru:['Схемы','Документы PDF Scheme Editor и их текущее состояние.','Схема','Обновлено','Ревизия или страницы'],en:['Schemes','PDF Scheme Editor documents and their current state.','Scheme','Updated','Revision or pages']}
};
var S={
discussions:{listTool:'shd_list_discussion_topics',listArgs:{limit:100},readTool:'shd_list_discussion_messages',detailKey:'topic_id',detailArgs:{limit:50},paths:['data'],sort:'desc'},
notifications:{listTool:'shd_list_notifications',listArgs:{limit:50,offset:0},paths:['data'],sort:'desc'},
scheduling:{listTool:'shd_get_scheduling_overview',listArgs:{limit:100},readTool:'shd_list_scheduling_bookings',detailKey:'booking_id',detailArgs:{limit:1},paths:['data.bookings','data.upcoming_bookings','data'],sort:'asc'},
inventory:{listTool:'shd_list_inventory_items',listArgs:{limit:100},readTool:'shd_get_inventory_item',detailKey:'item_id',paths:['data'],sort:'desc'},
agents:{listTool:'shd_agents_overview',listArgs:{},paths:['data.controllers','data.agents','data.user_agents','data'],sort:'asc'},
'status-page':{listTool:'shd_get_status_page_monitors',listArgs:{environment:'dev'},readTool:'shd_get_status_page_monitor_history',detailKey:'monitor_id',detailArgs:{limit:20},paths:['data'],nested:'monitors',sort:'asc'},
'project-db':{listTool:'shd_project_db_tables',listArgs:{},readTool:'shd_project_db_schema',detailKey:'table',paths:['data.tables','data'],contextRequired:true,sort:'asc'},
notes:{listTool:'shd_list_note_pages',listArgs:{limit:100},readTool:'shd_get_note_page',detailKey:'page_id',paths:['data'],sort:'desc'},
'project-overview':{listTool:'shd_get_project_context',listArgs:{include:['summary','tasks','discussions','documents','finance','terms','agents'],limit_per_section:100},paths:['data.sections.tasks','data.sections.discussions','data.sections.documents','data.sections.finance','data.sections.terms'],combinePaths:true,contextRequired:true,sort:'desc'},
files:{listTool:'shd_list_files',readTool:'shd_get_file_link',detailKey:'path',detailArgs:{download:false},detailSourceArgs:['project_code'],paths:['data'],contextRequired:true,sort:'desc'},
proposals:{listTool:'shd_list_proposals',listArgs:{limit:100},readTool:'shd_get_proposal',detailKey:'proposal_id',detailSourceArgs:[],paths:['data'],sort:'desc'},
terms:{listTool:'shd_list_terms_documents',readTool:'shd_get_terms_document',detailKey:'document_id',detailSourceArgs:['project_code'],paths:['data'],contextRequired:true,sort:'desc'},
activity:{listTool:'shd_get_realtime_events',listArgs:{module:'notifications',limit:100},paths:['data.events','events','data'],sort:'desc'},
organizations:{listTool:'shd_admin_list_organizations',listArgs:{limit:100},readTool:'shd_admin_get_organization_members',detailKey:'organization_id',detailSourceArgs:[],paths:['data'],sort:'asc'},
gitea:{listTool:'shd_list_gitea_issues',listArgs:{target:'site',limit:100},paths:['data'],sort:'desc'},
schemes:{listTool:'shd_list_scheme_documents',readTool:'shd_get_scheme_document',detailKey:'document',detailSourceArgs:['project'],paths:['data'],contextRequired:true,sort:'desc'}
};
var pending=new Map(),nextId=1,rows=[],state={module:DEFAULT_MODULE,config:Object.assign({},S[DEFAULT_MODULE]),lang:'ru',busy:false};
var e={title:document.getElementById('title'),desc:document.getElementById('desc'),search:document.getElementById('search'),refresh:document.getElementById('refresh'),status:document.getElementById('status'),summary:document.getElementById('summary'),rows:document.getElementById('rows'),empty:document.getElementById('empty'),details:document.getElementById('details'),detailTitle:document.getElementById('detail-title'),detailSub:document.getElementById('detail-sub'),grid:document.getElementById('grid'),close:document.getElementById('close'),ht:document.getElementById('head-title'),hs:document.getElementById('head-status'),hd:document.getElementById('head-date'),hv:document.getElementById('head-value')};
function lang(){var x=typeof window!=='undefined'&&window.openai&&window.openai.locale;x=String(x||document.documentElement.lang||'ru').toLowerCase();return x.indexOf('en')===0?'en':'ru'}
function t(k){return(C[state.lang]||C.ru)[k]||k}
function obj(x){return x&&typeof x==='object'&&!Array.isArray(x)}
function merge(a,b){return Object.assign({},a||{},b||{})}
function payload(x){if(!x)return null;if(Object.prototype.hasOwnProperty.call(x,'structuredContent'))return x.structuredContent;if(Object.prototype.hasOwnProperty.call(x,'toolOutput'))return x.toolOutput;return x}
function unwrap(x){if(x&&x.isError)throw Error(t('load'));var p=payload(x)||{};if(p.error)throw Error(String(p.error.message||p.error.detail||p.error));if(p.ok===false&&p.message)throw Error(String(p.message));return p}
function applyConfig(p){p=payload(p)||{};var m=obj(p.meta)?p.meta:{},w=obj(m.widget)?m.widget:{},mod=String(w.module||m.module||state.module||DEFAULT_MODULE);state.module=M[mod]?mod:DEFAULT_MODULE;state.config=merge(S[state.module],w);if(!obj(state.config.sourceArgs)&&obj(m.sourceArgs))state.config.sourceArgs=m.sourceArgs;state.lang=lang();labels()}
function labels(){var m=(M[state.module]||M.discussions)[state.lang]||M.discussions.ru;document.documentElement.lang=state.lang;e.title.textContent=m[0];e.desc.textContent=m[1];e.search.placeholder=t('search');e.refresh.textContent=t('refresh');e.empty.textContent=t('empty');e.close.setAttribute('aria-label',t('close'));e.ht.textContent=m[2];e.hs.textContent=t('status');e.hd.textContent=m[3];e.hv.textContent=m[4]}
function req(method,params){var o=typeof window!=='undefined'&&window.openai;if(method==='tools/call'&&o&&typeof o.callTool==='function')return Promise.resolve(o.callTool(params.name,params.arguments||{}));var id=nextId++;window.parent.postMessage({jsonrpc:'2.0',id:id,method:method,params:params||{}},'*');return new Promise(function(resolve,reject){var timeout=window.setTimeout(function(){pending.delete(id);reject(Error(t('load')))},12000);pending.set(id,{resolve:resolve,reject:reject,timeout:timeout})})}
function notify(method,params){window.parent.postMessage({jsonrpc:'2.0',method:method,params:params||{}},'*')}
function path(x,p){return String(p||'').split('.').reduce(function(v,k){return v&&typeof v==='object'?v[k]:undefined},x)}
function findArray(x,d){if(Array.isArray(x))return x;if(!obj(x)||d>2)return null;var keys=['items','records','bookings','upcoming_bookings','topics','notifications','pages','spaces','controllers','agents','user_agents','tables','monitors','data'];for(var i=0;i<keys.length;i++)if(Array.isArray(x[keys[i]]))return x[keys[i]];for(var k in x)if(Object.prototype.hasOwnProperty.call(x,k)){var a=findArray(x[k],d+1);if(a)return a}return null}
function flatten(a){var n=state.config.nested||state.config.nestedCollection;if(!n)return a;var out=[];a.forEach(function(parent){if(parent&&Array.isArray(parent[n]))parent[n].forEach(function(child){var x=Object.assign({},child);if(parent.name&&!x.group_name)x.group_name=parent.name;out.push(x)});else out.push(parent)});return out}
function list(x){var p=payload(x)||{};if(Array.isArray(p.items))return p.items;var ps=Array.isArray(state.config.paths)?state.config.paths:(Array.isArray(state.config.arrayPaths)?state.config.arrayPaths:[]),combined=[];for(var i=0;i<ps.length;i++){var c=path(p,ps[i]),a=Array.isArray(c)?c:findArray(c,0);if(a){if(state.config.combinePaths)combined=combined.concat(a);else return flatten(a)}}if(state.config.combinePaths&&combined.length)return flatten(combined);var d=p.data===undefined?p:p.data;return flatten(Array.isArray(d)?d:(findArray(d,0)||[]))}
function str(x){if(x===null||x===undefined||x==='')return'';if(typeof x==='object'){try{return JSON.stringify(x)}catch(err){return''}}return String(x)}
function id(x,i){var keys=['id','key','code','uuid','page_id','topic_id','booking_id','item_id','monitor_id','table'];for(var n=0;n<keys.length;n++)if(x&&x[keys[n]]!==undefined&&x[keys[n]]!==null&&x[keys[n]]!=='')return str(x[keys[n]]);return String(i+1)}
function status(x){var v=x&&x.status;if(v&&typeof v==='object')v=v.name||v.label||v.code||v.id;return str(v||(x&&(x.state||x.phase||x.health))||'')}
function title(x,i){return str(x&&(x.title||x.name||x.subject||x.display_name||x.label||x.table_name||x.table||x.key))||(t('records')+' '+(i+1))}
function date(x){return str(x&&(x.starts_at||x.start_at||x.due_date||x.checked_at||x.updated_at||x.created_at||x.sent_at||x.published_at||x.date))}
function value(x){if(!x)return'';if(x.availability_percent!==undefined&&x.availability_percent!==null)return str(x.availability_percent)+'%';if(x.quantity!==undefined&&x.quantity!==null)return str(x.quantity);if(x.record_count!==undefined&&x.record_count!==null)return str(x.record_count);return str(x.value!==undefined?x.value:(x.guest_name||x.space_name||x.group_name||x.controller_type||x.type||x.source||x.module||''))}
function sub(x){return str(x&&(x.subtitle||x.project_code||x.space_name||x.channel_name||x.group_name||x.code||x.email))}
function normalize(x,i){var y=obj(x)?Object.assign({},x):{value:x};y.id=id(x,i);y.title=title(x,i);y.subtitle=sub(x);y.status=status(x);y.date=date(x);y.value=value(x);return y}
function dateText(x){if(!x)return'—';var n=Date.parse(x);if(Number.isNaN(n))return String(x);try{return new Intl.DateTimeFormat(state.lang,{dateStyle:'medium',timeStyle:'short'}).format(new Date(n))}catch(err){return String(x)}}
function make(tag,v,cls){var n=document.createElement(tag);if(cls)n.className=cls;if(v!==undefined)n.textContent=v;return n}
function detailText(x){return x===null||x===undefined||x===''?'—':str(x)}
function renderSummary(){e.summary.replaceChildren();var n=make('span');n.append(make('strong',String(rows.length)),' '+t('records'));e.summary.append(n);var c={};rows.forEach(function(x){if(x.status)c[x.status]=(c[x.status]||0)+1});Object.keys(c).slice(0,3).forEach(function(k){var n=make('span');n.append(make('strong',String(c[k])),' '+k);e.summary.append(n)})}
function renderRows(){var q=e.search.value.trim().toLowerCase(),a=rows.filter(function(x){return!q||[x.title,x.subtitle,x.status,x.id,x.value].map(str).join(' ').toLowerCase().indexOf(q)!==-1});e.rows.replaceChildren();e.empty.hidden=a.length!==0;a.forEach(function(x){var r=document.createElement('tr');r.className='record';r.tabIndex=0;r.setAttribute('role','button');r.setAttribute('aria-label',x.title);var n=make('td');n.append(make('span',x.title,'title'));if(x.subtitle)n.append(make('span',x.subtitle,'sub'));var s=make('span',x.status||'—','pill');if(x.status)s.classList.add(String(x.status).toLowerCase().replace(/[^a-z0-9_-]+/g,'-'));r.append(n,make('td').appendChild(s).parentNode,make('td',dateText(x.date)),make('td',x.value||'—'));r.onclick=function(){details(x)};r.onkeydown=function(ev){if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();details(x)}};e.rows.append(r)});renderSummary()}
function render(x){var p=unwrap(x);applyConfig(p);rows=list(p).map(normalize);var dir=state.config.sort==='asc'?1:-1;rows.sort(function(a,b){var x=Date.parse(a.date),y=Date.parse(b.date);if(!Number.isNaN(x)&&!Number.isNaN(y)&&x!==y)return(x-y)*dir;return a.title.localeCompare(b.title,state.lang)*dir});renderRows()}
function setStatus(x,bad){e.status.textContent=x||'';e.status.className=bad?'status error':'status'}
function add(k,v){var d=make('div',undefined,'detail');d.append(make('span',k,'label'),make('span',detailText(v),'value'));e.grid.append(d)}
function show(x){e.detailTitle.textContent=x.title||'SHD';e.detailSub.textContent=x.subtitle||x.id||'';e.grid.replaceChildren();add(t('id'),x.id);if(x.status)add(t('status'),x.status);if(x.date)add(t('date'),dateText(x.date));if(x.value)add(t('value'),x.value);Object.keys(x).filter(function(k){return['id','title','subtitle','status','date','value','meta'].indexOf(k)===-1}).slice(0,8).forEach(function(k){add(k,x[k])});e.details.hidden=false}
function details(x){show(x);if(!state.config.readTool)return;var k=state.config.detailKey;if(!k)return;var v=x[k]===undefined?x.id:x[k];if(v===undefined||v===null||v==='')return;if(state.module==='status-page'&&!/^\d+$/.test(String(v)))return;var a={};if(Array.isArray(state.config.detailSourceArgs)){state.config.detailSourceArgs.forEach(function(name){if(state.config.sourceArgs&&state.config.sourceArgs[name]!==undefined)a[name]=state.config.sourceArgs[name]})}else a=merge(state.config.sourceArgs,state.config.detailArgs);a=merge(a,state.config.detailArgs);a[k]=k==='table'?String(v):(Number(v)||v);req('tools/call',{name:state.config.readTool,arguments:a}).then(function(result){var p=unwrap(result),d=p.data===undefined?p:p.data;e.grid.replaceChildren();if(Array.isArray(d)){add(t('records'),d.length);d.slice(0,6).forEach(function(q,i){add(String(i+1),q&&(q.message||q.body||q.content||q.title||q))})}else if(obj(d))Object.keys(d).slice(0,12).forEach(function(q){add(q,d[q])});else add(t('details'),d)}).catch(function(err){setStatus(err.message||t('detail'),true)})}
function refresh(){if(state.busy)return;var a=merge(state.config.listArgs,state.config.sourceArgs);if(state.config.contextRequired&&!a.base&&!a.project_code&&!a.project){setStatus(t('context'),true);return}if(!state.config.listTool)return;state.busy=true;e.refresh.disabled=true;setStatus(t('loading'),false);req('tools/call',{name:state.config.listTool,arguments:a}).then(function(x){render(x);setStatus('',false)}).catch(function(err){setStatus(err.message||t('load'),true)}).finally(function(){state.busy=false;e.refresh.disabled=false})}
function init(){return req('ui/initialize',{protocolVersion:'2025-11-25',appInfo:{name:'SHD '+(M[state.module]||M.discussions).ru[0],title:'SHD MCP',version:'1.0.0',websiteUrl:'https://shd.xyz.su'},appCapabilities:{}}).then(function(){notify('ui/notifications/initialized',{})}).catch(function(){})}
window.addEventListener('message',function(ev){if(ev.source!==window.parent)return;var m=ev.data;if(!m||m.jsonrpc!=='2.0')return;if(m.id!==undefined&&pending.has(m.id)){var q=pending.get(m.id);pending.delete(m.id);window.clearTimeout(q.timeout);if(m.error)q.reject(Error(m.error.message||t('load')));else q.resolve(m.result);return}if(m.method==='ui/notifications/tool-input'&&!rows.length){try{render(m.params||{})}catch(err){setStatus(err.message||t('load'),true)}}if(m.method==='ui/notifications/tool-result'){try{render(m.params&&m.params.structuredContent)}catch(err){setStatus(err.message||t('load'),true)}}},{passive:true});
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
}());
</script>
<script data-shd-widget-write-actions="js">
(function () {
'use strict';
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
var copy = {
ru: {
actions: 'Действия',
chooseAction: 'Выберите действие',
selectRecord: 'Сначала выберите запись.',
submit: 'Сохранить',
cancel: 'Отмена',
confirm: 'Подтверждаю действие',
warning: 'Проверьте данные перед записью.',
destructive: 'Операция может изменить или удалить данные.',
required: 'Заполните обязательное поле.',
invalidJson: 'Введите корректный JSON.',
success: 'Изменения сохранены.',
failure: 'Не удалось сохранить изменения.',
loading: 'Сохраняю…',
noActions: 'Для этого виджета нет доступных действий.',
refreshed: 'Список обновлён.',
fieldValue: 'Значение',
},
en: {
actions: 'Actions',
chooseAction: 'Choose an action',
selectRecord: 'Select a record first.',
submit: 'Save',
cancel: 'Cancel',
confirm: 'I confirm this action',
warning: 'Review the values before writing.',
destructive: 'This operation may change or remove data.',
required: 'Fill in the required field.',
invalidJson: 'Enter valid JSON.',
success: 'Changes saved.',
failure: 'Could not save changes.',
loading: 'Saving…',
noActions: 'No actions are available for this widget.',
refreshed: 'List refreshed.',
fieldValue: 'Value',
},
};
var state = {
widget: {},
records: [],
selected: null,
action: null,
inputs: {},
busy: false,
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
};
var pending = new Map();
var nextRequestId = 1;
var elements = {};
function t(key) {
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
}
function object(value) {
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}
function normalize(value) {
if (!value) return null;
if (value.structuredContent) return normalize(value.structuredContent);
if (value.toolOutput) return normalize(value.toolOutput);
if (value.toolInput) return normalize(value.toolInput);
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
return value;
}
function widgetFrom(value) {
var payload = normalize(value) || {};
var meta = object(payload.meta) ? payload.meta : {};
return object(meta.widget) ? meta.widget : {};
}
function recordsFrom(value) {
var payload = normalize(value) || {};
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
if (!Array.isArray(data)) data = payload.data;
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
}
function readPath(value, path) {
if (!value || path === undefined || path === null) return undefined;
var parts = String(path).split('.');
var current = value;
for (var index = 0; index < parts.length; index += 1) {
if (current === undefined || current === null) return undefined;
current = current[parts[index]];
}
return current;
}
function firstValue(paths) {
var list = Array.isArray(paths) ? paths : [paths];
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
var sourceObject = sources[sourceIndex];
if (!sourceObject) continue;
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
var value = readPath(sourceObject, list[pathIndex]);
if (value !== undefined && value !== null && value !== '') return value;
}
}
return undefined;
}
function text(value) {
if (value === undefined || value === null) return '';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
}
function label(value) {
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
return String(value || t('fieldValue'));
}
function actionLabel(action) {
return label(action && action.label) || action.id || action.tool || t('actions');
}
function hasTargets(action) {
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
}
function actionList() {
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
return action && action.tool;
}) : [];
}
function request(method, params) {
var openai = typeof window !== 'undefined' ? window.openai : undefined;
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
return openai.callTool(params.name, params.arguments || {});
}
var id = nextRequestId++;
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
return new Promise(function (resolve, reject) {
var timeout = window.setTimeout(function () {
pending.delete(id);
reject(new Error(t('failure')));
}, 15000);
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
});
}
function unwrap(value) {
var payload = normalize(value);
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
if (payload && payload.error) throw new Error(text(payload.error));
return payload || value;
}
function make(tag, value, className) {
var node = document.createElement(tag);
if (className) node.className = className;
if (value !== undefined) node.textContent = value;
return node;
}
function ensurePanel() {
if (elements.panel) return;
var root = document.querySelector('main') || document.body;
var panel = make('section', undefined, 'shd-widget-actions');
panel.hidden = true;
panel.setAttribute('aria-live', 'polite');
var header = make('div', undefined, 'shd-widget-actions-header');
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
var select = document.createElement('select');
select.className = 'shd-widget-actions-select';
select.setAttribute('aria-label', t('actions'));
var form = make('form', undefined, 'shd-widget-actions-form');
var status = make('div', '', 'shd-widget-actions-status');
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
submit.type = 'submit';
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
cancel.type = 'button';
buttons.append(submit, cancel);
panel.append(header, select, form, status, buttons);
root.append(panel);
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
select.addEventListener('change', function () {
var selected = actionList().find(function (action) { return action.id === select.value; });
state.action = selected || null;
renderForm();
});
cancel.addEventListener('click', function () {
state.action = null;
select.value = '';
renderForm();
});
form.addEventListener('submit', function (event) {
event.preventDefault();
submitAction();
});
}
function inputValue(fieldConfig) {
var value = firstValue(fieldConfig.source || []);
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
return value;
}
function createInput(fieldConfig) {
var type = fieldConfig.type || 'text';
var input;
if (type === 'textarea' || type === 'json') {
input = document.createElement('textarea');
input.rows = type === 'json' ? 4 : 3;
if (type === 'json') input.placeholder = '{ }';
} else if (type === 'select') {
input = document.createElement('select');
(fieldConfig.options || []).forEach(function (option) {
var item = document.createElement('option');
item.value = String(option[0]);
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
input.append(item);
});
} else {
input = document.createElement('input');
input.type = type === 'number' || type === 'email' ? type : 'text';
}
input.className = 'shd-widget-actions-input';
input.name = fieldConfig.name;
var value = inputValue(fieldConfig);
if (type === 'checkbox') {
input.type = 'checkbox';
input.checked = value === true || value === 'true' || value === 1 || value === '1';
} else if (value !== undefined && value !== null) {
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
}
if (fieldConfig.required) input.required = true;
return input;
}
function renderForm() {
ensurePanel();
var selectedActionId = state.action ? state.action.id : '';
elements.select.replaceChildren();
var placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = t('chooseAction');
elements.select.append(placeholder);
actionList().forEach(function (action) {
var option = document.createElement('option');
option.value = action.id;
option.textContent = actionLabel(action);
elements.select.append(option);
});
elements.select.value = selectedActionId;
elements.select.hidden = actionList().length === 0;
elements.form.replaceChildren();
elements.status.textContent = '';
elements.submit.disabled = state.busy;
var action = state.action;
if (!action) {
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
elements.submit.hidden = true;
elements.cancel.hidden = true;
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
}
return;
}
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
elements.submit.hidden = false;
elements.cancel.hidden = false;
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
(action.fields || []).forEach(function (fieldConfig) {
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
var wrapper = make('label', undefined, 'shd-widget-actions-field');
wrapper.append(make('span', label(fieldConfig.label)));
wrapper.append(createInput(fieldConfig));
elements.form.append(wrapper);
});
if (action.confirm) {
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
var confirm = document.createElement('input');
confirm.type = 'checkbox';
confirm.name = '__confirm';
confirm.required = true;
confirmLabel.append(confirm, make('span', t('confirm')));
elements.form.append(confirmLabel);
}
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
elements.form.append(notice);
}
function coerce(value, type) {
if (type === 'number') {
var number = Number(value);
return Number.isFinite(number) ? number : value;
}
if (type === 'checkbox') return Boolean(value);
if (type === 'json') {
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
}
return value;
}
function targetType(name) {
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
}
function targetList(action) {
if (Array.isArray(action.targets)) return action.targets;
return action.target ? [action.target] : [];
}
function collectArguments(action) {
var args = Object.assign({}, action.fixed || {});
(action.context || []).forEach(function (name) {
var value = firstValue([name]);
if (value !== undefined && value !== null && value !== '') args[name] = value;
});
targetList(action).forEach(function (targetConfig) {
var value = firstValue(targetConfig.source || targetConfig.name);
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
});
(action.fields || []).forEach(function (fieldConfig) {
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
return node.getAttribute('name') === fieldConfig.name;
});
if (!input) return;
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
if (raw === '' && !input.checked && !fieldConfig.required) return;
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
});
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
if (action.confirmArg) args.confirm = true;
return args;
}
function setStatus(message, error) {
elements.status.textContent = message || '';
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
}
function submitAction() {
if (state.busy || !state.action) return;
var action = state.action;
var args;
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
state.busy = true;
elements.submit.disabled = true;
elements.cancel.disabled = true;
setStatus(t('loading'), false);
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
unwrap(result);
setStatus(t('success'), false);
var refresh = document.getElementById('refresh');
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
}).catch(function (error) {
setStatus(error && error.message ? error.message : t('failure'), true);
}).finally(function () {
state.busy = false;
elements.submit.disabled = false;
elements.cancel.disabled = false;
});
}
function recordFromNode(node) {
var content = String(node.textContent || '').toLowerCase();
var exact = state.records.find(function (item) {
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
return id && content.indexOf(id) !== -1;
});
if (exact) return exact;
return state.records.find(function (item) {
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
return title && content.indexOf(title) !== -1;
}) || null;
}
function update(value) {
var payload = normalize(value) || {};
var widget = widgetFrom(payload);
if (Object.keys(widget).length) state.widget = widget;
var records = recordsFrom(payload);
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
ensurePanel();
var actions = actionList();
elements.panel.hidden = actions.length === 0;
if (actions.length && !state.action) renderForm();
else if (actions.length) renderForm();
}
document.addEventListener('click', function (event) {
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
if (!node || (elements.panel && elements.panel.contains(node))) return;
var item = recordFromNode(node);
if (!item) return;
state.selected = item;
ensurePanel();
renderForm();
}, true);
window.addEventListener('message', function (event) {
if (event.source !== window.parent) return;
var message = event.data;
if (!message || message.jsonrpc !== '2.0') return;
if (message.id !== undefined && pending.has(message.id)) {
var requestState = pending.get(message.id);
pending.delete(message.id);
window.clearTimeout(requestState.timeout);
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
else requestState.resolve(message.result);
return;
}
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
}, { passive: true });
ensurePanel();
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
if (initial) update(initial);
}());
</script>
</body>
</html>
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"name": "shd-activity",
"version": "1.0.0",
"resources": [
{
"file": "index.html",
"bytes": 45934,
"gzipBytes": 13581,
"mediaType": "text/html;profile=mcp-app",
"uri": "ui://shd/activity/v1.html",
"sha256": "766b830b207f2929c16635f24081bf11e9e69f2c9a4cb261157a79a6cfd4dc63"
}
],
"license": "../../../../../LICENSE",
"source": {
"repository": "https://github.com/bulava92/shd-mcp-plugin",
"path": "plugins/shd-mcp-plugin/widgets/activity/v1/index.html"
}
}
@@ -0,0 +1 @@
d7042f24d6bce4b94db2726c252def333bb99c619295c85a8fb4855788c35373 index.html
@@ -0,0 +1,5 @@
# Agents health widget
This artifact is served by the SHD MCP backend as ui://shd/agents-health/v1.html.
The HTML is standalone, uses the MCP Apps bridge first, and keeps window.openai as a compatibility extension. It has no external script or network dependency. The widget provides a read-only agent and controller health overview.
@@ -0,0 +1,617 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SHD MCP</title>
<style>
:root{color-scheme:light dark;--bg:#f8fafc;--surface:#e8edf2;--hover:#e1e8ef;--line:rgba(31,41,55,.16);--ink:#1d1f25;--muted:#616670;--accent:#535963;--soft:rgba(75,85,99,.12);--bad:#b42318;--good:#2f855a;--button:#fff;--shadow:0 10px 26px rgba(31,41,55,.1)}
*{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}.shell{max-width:980px;margin:auto;padding:22px}.card{padding:20px;border:1px solid var(--line);border-radius:18px;background:var(--surface);box-shadow:var(--shadow)}
.eyebrow{color:var(--muted);font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}h1{margin:4px 0 5px;font-size:25px;line-height:1.15}.lede{margin:0;color:var(--muted)}.summary{display:flex;gap:8px;flex-wrap:wrap;margin:18px 0 14px}.summary span{padding:5px 9px;border:1px solid var(--line);border-radius:999px;background:var(--hover);color:var(--muted)}.summary strong{color:var(--ink)}
.toolbar{display:flex;gap:8px;margin-bottom:10px}.search{min-width:0;flex:1;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:inherit;outline:0}.search:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--soft)}button{cursor:pointer}.refresh{padding:10px 14px;border:1px solid var(--accent);border-radius:10px;background:var(--accent);color:var(--button);font-weight:650}.refresh:disabled{opacity:.6;cursor:wait}.status{min-height:22px;color:var(--muted)}.error{color:var(--bad)}
.table-wrap{max-height:460px;overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--bg)}table{width:100%;border-collapse:collapse;min-width:560px}th,td{padding:11px 13px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}th{position:sticky;top:0;background:var(--hover);color:var(--muted);font-size:12px;font-weight:650;z-index:1}tbody tr:last-child td{border-bottom:0}.record{cursor:pointer;outline:0}.record:hover,.record:focus{background:var(--soft)}.title{font-weight:650}.sub{display:block;color:var(--muted);font-size:12px;margin-top:2px}.pill{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--soft);color:var(--accent);font-size:12px;white-space:nowrap}.pill.up,.pill.ok,.pill.active{color:var(--good);background:rgba(47,133,90,.14)}.pill.down,.pill.error,.pill.overdue{color:var(--bad);background:rgba(180,35,24,.12)}.empty{padding:24px;text-align:center;color:var(--muted)}
.visual[hidden]{display:none}.visual{max-height:460px;overflow:auto;border:1px solid var(--line);border-radius:12px;background:var(--bg);padding:12px}.visual-card{display:block;width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:10px;background:var(--surface);color:inherit;text-align:left;cursor:pointer}.visual-card:hover,.visual-card:focus{background:var(--hover);border-color:var(--accent);outline:0}.visual-title{display:block;font-weight:650}.visual-date,.visual-value{display:block;color:var(--muted);font-size:12px;margin-top:4px}.visual-line{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:7px}.kanban{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(190px,1fr);gap:10px;overflow-x:auto;align-items:start}.kanban-col{min-width:190px;padding:10px;border:1px solid var(--line);border-radius:12px;background:var(--surface)}.kanban-head{display:flex;justify-content:space-between;gap:8px;margin-bottom:8px;font-weight:650}.kanban-list{display:grid;gap:8px}.metric-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(145px,1fr));gap:10px;margin-bottom:12px}.metric{padding:12px;border:1px solid var(--line);border-radius:10px;background:var(--surface)}.metric-label{display:block;color:var(--muted);font-size:12px}.metric-value{display:block;font-size:22px;font-weight:700;margin-top:4px}.timeline{display:grid;gap:0}.timeline-item{display:grid;grid-template-columns:18px minmax(0,1fr);gap:10px;position:relative;padding-bottom:10px}.timeline-rail{position:absolute;left:8px;top:16px;bottom:0;width:2px;background:var(--line)}.timeline-item:last-child .timeline-rail{display:none}.timeline-dot{width:16px;height:16px;border-radius:50%;background:var(--accent);border:3px solid var(--soft);z-index:1}.timeline-content{min-width:0}.timeline-meta{color:var(--muted);font-size:12px;margin-bottom:5px}.funnel{display:grid;gap:9px}.funnel-row{display:grid;grid-template-columns:minmax(100px,1fr) minmax(120px,2fr) auto;gap:10px;align-items:center}.funnel-fill{width:100%;height:10px;accent-color:var(--accent)}.progress-card{display:grid;gap:7px;padding:11px;border:1px solid var(--line);border-radius:10px;background:var(--surface)}.progress{width:100%;height:10px;accent-color:var(--accent)}.calendar{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px}.calendar-day{padding:10px;border:1px solid var(--line);border-radius:12px;background:var(--surface);display:grid;gap:8px;align-content:start}.calendar-head{font-weight:650}.matrix-wrap{overflow:auto}.matrix-table{width:100%;border-collapse:collapse;min-width:560px}.matrix-table th,.matrix-table td{padding:9px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}.matrix-table th{color:var(--muted);font-size:12px;background:var(--hover)}.tree{display:grid;gap:7px}.tree-children{display:grid;gap:7px;padding-left:18px;border-left:1px solid var(--line)}.preview-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:10px}.preview-card{padding:12px;border:1px solid var(--line);border-radius:10px;background:var(--surface)}.preview-fields{display:grid;gap:5px;margin-top:8px}.preview-field{font-size:12px;overflow-wrap:anywhere}.preview-field strong{color:var(--muted);font-weight:500}@media(max-width:640px){.funnel-row{grid-template-columns:1fr auto}.funnel-fill{grid-column:1/-1}}
.details{margin-top:14px;padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--bg)}.details[hidden]{display:none}.detail-head{display:flex;justify-content:space-between;gap:12px;align-items:start}.details h2{margin:0;font-size:18px}.close{border:0;background:transparent;color:var(--muted);font-size:20px}.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-top:14px}.detail{padding:9px;border-radius:8px;background:var(--surface)}.label{display:block;color:var(--muted);font-size:11px;margin-bottom:3px}.value{overflow-wrap:anywhere;white-space:pre-wrap}@media(max-width:640px){.shell{padding:12px}.card{padding:15px}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto}}
@media(prefers-color-scheme:dark){:root{--bg:#202020;--surface:#292929;--hover:#303336;--line:#43474c;--ink:#eaecf0;--muted:#96999d;--accent:#c7b07a;--soft:rgba(199,176,122,.18);--bad:#ff9a93;--good:#83c995;--button:#241f14;--shadow:0 10px 26px rgba(0,0,0,.34)}}
</style>
<style data-shd-widget-write-actions="css">
.shd-widget-actions{margin-top:14px;padding:14px;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:11px;background:var(--surface,#e8edf2);color:var(--text,#1d1f25)}
.shd-widget-actions[hidden]{display:none}
.shd-widget-actions-header{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
.shd-widget-actions-selection{overflow:hidden;color:var(--muted,#616670);font-size:12px;text-overflow:ellipsis;white-space:nowrap}
.shd-widget-actions-select,.shd-widget-actions-input{width:100%;border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 10px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25)}
.shd-widget-actions-select:focus,.shd-widget-actions-input:focus{border-color:var(--accent,#535963);outline:0;box-shadow:0 0 0 3px var(--accent-soft,rgba(75,85,99,.12))}
.shd-widget-actions-form{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin-top:10px}
.shd-widget-actions-field{display:grid;gap:4px;color:var(--muted,#616670);font-size:11px}
.shd-widget-actions-field:has(textarea){grid-column:span 2}
.shd-widget-actions-field input[type=checkbox]{width:18px;height:18px;accent-color:var(--accent,#535963)}
.shd-widget-actions-confirm{display:flex;grid-column:1/-1;align-items:center;gap:7px;color:var(--text,#1d1f25);font-size:12px}
.shd-widget-actions-warning,.shd-widget-actions-hint{grid-column:1/-1;color:var(--muted,#616670);font-size:12px}
.shd-widget-actions-danger{color:var(--danger,#b42318)}
.shd-widget-actions-status{min-height:18px;margin-top:9px;color:var(--muted,#616670);font-size:12px}
.shd-widget-actions-status-error{color:var(--danger,#b42318)}
.shd-widget-actions-buttons{display:flex;justify-content:flex-end;gap:8px;margin-top:10px}
.shd-widget-actions-buttons button{border:1px solid var(--border,rgba(31,41,55,.16));border-radius:8px;padding:8px 11px;background:var(--bg,#f8fafc);color:var(--text,#1d1f25);cursor:pointer}
.shd-widget-actions-buttons button:hover{background:var(--surface-hover,#e1e8ef)}
.shd-widget-actions-buttons button:disabled{cursor:wait;opacity:.62}
@media(max-width:600px){.shd-widget-actions-field:has(textarea){grid-column:auto}.shd-widget-actions-header{display:block}.shd-widget-actions-selection{display:block;margin-top:3px}}
</style>
</head>
<body>
<main class="shell" data-widget-module="agents-health"><section class="card">
<div class="eyebrow">SHD MCP</div><h1 id="title">SHD</h1><p class="lede" id="desc"></p><div class="summary" id="summary"></div>
<div class="toolbar"><input class="search" id="search" type="search" autocomplete="off"><button class="refresh" id="refresh" type="button"></button></div>
<div class="status" id="status" role="status" aria-live="polite"></div>
<div class="table-wrap"><table><thead><tr><th id="head-title"></th><th id="head-status"></th><th id="head-date"></th><th id="head-value"></th></tr></thead><tbody id="rows"></tbody></table><div class="empty" id="empty" hidden></div></div><div class="visual" id="visual" hidden></div>
<section class="details" id="details" hidden aria-live="polite"><div class="detail-head"><div><h2 id="detail-title"></h2><div class="sub" id="detail-sub"></div></div><button class="close" id="close" type="button">×</button></div><div class="grid" id="grid"></div></section>
</section></main>
<script>
(function(){
'use strict';
var DEFAULT_MODULE='agents-health';
var C={ru:{refresh:'Обновить',search:'Поиск по названию, статусу или ID',loading:'Загрузка…',empty:'Нет данных для отображения.',records:'записей',status:'Статус',date:'Дата',value:'Детали',id:'ID',details:'Детали',close:'Закрыть',load:'Не удалось загрузить данные.',detail:'Не удалось загрузить детали.',context:'Для обновления нужен контекст источника',total:'Всего',amount:'Сумма',planned:'План',capacity:'Ёмкость',utilization:'Загрузка',overload:'Перегрузка',warnings:'Предупреждения',complete:'Готово',pending:'Ожидают',approved:'Согласовано',rejected:'Отклонено',progress:'Прогресс',score:'Оценка',member:'Участник',role:'Роль',permissions:'Разрешения',preview:'Предпросмотр',noWarnings:'Предупреждений нет.',health:'Состояние'},en:{refresh:'Refresh',search:'Search by name, status or ID',loading:'Loading…',empty:'No data to display.',records:'records',status:'Status',date:'Date',value:'Details',id:'ID',details:'Details',close:'Close',load:'Could not load data.',detail:'Could not load details.',context:'A source context is required to refresh.',total:'Total',amount:'Amount',planned:'Planned',capacity:'Capacity',utilization:'Utilization',overload:'Overload',warnings:'Warnings',complete:'Complete',pending:'Pending',approved:'Approved',rejected:'Rejected',progress:'Progress',score:'Score',member:'Member',role:'Role',permissions:'Permissions',preview:'Preview',noWarnings:'No warnings.',health:'Health'}};
var M={
discussions:{ru:['Обсуждения','Темы обсуждений с поиском и просмотром сообщений.','Тема','Обновлено','Сообщение'],en:['Discussions','Discussion topics with search and message details.','Topic','Updated','Message']},
notifications:{ru:['Уведомления','Последние уведомления и события модулей.','Уведомление','Время','Источник'],en:['Notifications','Recent notifications and module events.','Notification','Time','Source']},
scheduling:{ru:['Расписание','Ближайшие события и записи из расписания.','Событие','Начало','Участник'],en:['Scheduling','Upcoming events and bookings from scheduling.','Event','Starts','Guest']},
inventory:{ru:['Склад','Складские позиции, остатки и статусы.','Позиция','Обновлено','Количество'],en:['Inventory','Stock items, quantities and statuses.','Item','Updated','Quantity']},
agents:{ru:['Агенты и контроллеры','Доступные агенты, контроллеры и их текущее состояние.','Агент или контроллер','Проверено','Тип'],en:['Agents & Controllers','Available agents, controllers and their current state.','Agent or controller','Checked','Type']},
'status-page':{ru:['Status Page','Состояние доступности сервисов и мониторов.','Монитор','Проверено','Доступность'],en:['Status Page','Service availability and monitor state.','Monitor','Checked','Availability']},
'project-db':{ru:['Project DB','Каталог таблиц Project DB с просмотром схемы.','Таблица','Обновлено','Записей'],en:['Project DB','Project DB table catalog with schema details.','Table','Updated','Records']},
notes:{ru:['Заметки','Страницы Notes с поиском и просмотром содержимого.','Страница','Обновлено','Пространство'],en:['Notes','Notes pages with search and content details.','Page','Updated','Space']},
'project-overview':{ru:['Обзор проекта','Сводные данные по разделам выбранного проекта.','Раздел','Обновлено','Элементы'],en:['Project overview','Cross-module records for the selected project.','Section','Updated','Items']},
files:{ru:['Файлы','Файлы выбранного проекта с поиском и безопасным просмотром ссылок.','Файл','Обновлено','Размер или тип'],en:['Files','Files from the selected project with search and safe links.','File','Updated','Size or type']},
proposals:{ru:['Предложения','Предложения и сметы с поиском, статусами и деталями.','Предложение','Обновлено','Сумма или номер'],en:['Proposals','Proposals with search, statuses and details.','Proposal','Updated','Amount or number']},
terms:{ru:['Terms / ТЗ','Документы технического задания и их ревизии.','Документ','Обновлено','Страницы или ревизия'],en:['Terms documents','Terms of Reference documents and revisions.','Document','Updated','Pages or revision']},
activity:{ru:['Активность','Авторизованные realtime-события SHD по выбранному scope.','Событие','Время','Модуль или тип'],en:['Activity','Authenticated SHD realtime events for the selected scope.','Event','Time','Module or type']},
organizations:{ru:['Организации и доступ','Организации, роли и безопасный просмотр участников.','Организация','Обновлено','Роль или тип'],en:['Organizations & access','Organizations, roles and safe membership details.','Organization','Updated','Role or type']},
gitea:{ru:['Gitea Issues','Открытые задачи разрешённого site или project репозитория.','Issue','Обновлено','Состояние'],en:['Gitea Issues','Open issues for an allowed site or project repository.','Issue','Updated','State']},
schemes:{ru:['Схемы','Документы PDF Scheme Editor и их текущее состояние.','Схема','Обновлено','Ревизия или страницы'],en:['Schemes','PDF Scheme Editor documents and their current state.','Scheme','Updated','Revision or pages']},
'task-kanban':{ru:['Задачи Kanban','Задачи по колонкам статусов с поиском и деталями.','Задача','Срок','Исполнитель'],en:['Task Kanban','Tasks grouped by workflow status with search and details.','Task','Due','Assignee']},
'team-workload':{ru:['Загрузка команды','Плановая нагрузка, ёмкость и утилизация команды.','Участник','Период','Часы или процент'],en:['Team workload','Planned workload, capacity and team utilization.','Member','Period','Hours or percent']},
'project-timeline':{ru:['Timeline проекта','Задачи проекта на временной ленте.','Задача','Срок','Статус'],en:['Project timeline','Project tasks on a chronological timeline.','Task','Due','Status']},
'crm-funnel':{ru:['CRM-воронка','Сделки CRM, сгруппированные по стадиям.','Стадия','Сделки','Сумма'],en:['CRM funnel','CRM deals grouped by pipeline stage.','Stage','Deals','Amount']},
'finance-dashboard':{ru:['Финансовый дашборд','Суммы, статусы и документы финансового модуля.','Документ','Обновлено','Сумма'],en:['Finance dashboard','Finance amounts, statuses and documents.','Document','Updated','Amount']},
'proposal-approvals':{ru:['Согласования предложений','Очередь предложений и текущие approval-статусы.','Предложение','Обновлено','Approval'],en:['Proposal approvals','Proposal queue and current approval states.','Proposal','Updated','Approval']},
'documents-completeness':{ru:['Полнота документов','Score, ревизии и состояние Terms-документов.','Документ','Обновлено','Полнота'],en:['Documents completeness','Scores, revisions and Terms document state.','Document','Updated','Completeness']},
'agents-health':{ru:['Здоровье агентов','Состояние агентов и контроллеров.','Агент или контроллер','Проверено','Health'],en:['Agents health','Agent and controller health state.','Agent or controller','Checked','Health']},
'schedule-calendar':{ru:['Календарь расписания','Ближайшие записи, сгруппированные по дням.','Событие','День','Участник'],en:['Schedule calendar','Upcoming bookings grouped by day.','Event','Day','Guest']},
'acl-matrix':{ru:['ACL-матрица','Участники организации, роли и permissions.','Участник','Роль','Права'],en:['ACL matrix','Organization members, roles and permissions.','Member','Role','Permissions']},
'inventory-warnings':{ru:['Предупреждения склада','Низкие остатки и критические складские позиции.','Позиция','Обновлено','Остаток'],en:['Inventory warnings','Low-stock and critical inventory items.','Item','Updated','Stock']},
'gitea-board':{ru:['Gitea-доска','Issues по состояниям разрешённого репозитория.','Issue','Состояние','Ссылка'],en:['Gitea board','Issues grouped by state for an allowed repository.','Issue','State','Link']},
'schemes-progress':{ru:['Прогресс схем','Документы схем, ревизии, страницы и completeness.','Схема','Обновлено','Прогресс'],en:['Schemes progress','Scheme documents, revisions, pages and completeness.','Scheme','Updated','Progress']},
'notes-tree':{ru:['Дерево Notes','Пространства Notes и их иерархия.','Узел','Обновлено','Тип'],en:['Notes tree','Notes spaces and their hierarchy.','Node','Updated','Type']},
'project-db-preview':{ru:['Project DB preview','Предпросмотр записей выбранной таблицы Project DB.','Запись','Обновлено','Поля'],en:['Project DB preview','Preview of records from the selected Project DB table.','Record','Updated','Fields']}
};
var S={
discussions:{listTool:'shd_list_discussion_topics',listArgs:{limit:100},readTool:'shd_list_discussion_messages',detailKey:'topic_id',detailArgs:{limit:50},paths:['data'],sort:'desc'},
notifications:{listTool:'shd_list_notifications',listArgs:{limit:50,offset:0},paths:['data'],sort:'desc'},
scheduling:{listTool:'shd_get_scheduling_overview',listArgs:{limit:100},readTool:'shd_list_scheduling_bookings',detailKey:'booking_id',detailArgs:{limit:1},paths:['data.bookings','data.upcoming_bookings','data'],sort:'asc'},
inventory:{listTool:'shd_list_inventory_items',listArgs:{limit:100},readTool:'shd_get_inventory_item',detailKey:'item_id',paths:['data'],sort:'desc'},
agents:{listTool:'shd_agents_overview',listArgs:{},paths:['data.controllers','data.agents','data.user_agents','data'],sort:'asc'},
'status-page':{listTool:'shd_get_status_page_monitors',listArgs:{environment:'dev'},readTool:'shd_get_status_page_monitor_history',detailKey:'monitor_id',detailArgs:{limit:20},paths:['data'],nested:'monitors',sort:'asc'},
'project-db':{listTool:'shd_project_db_tables',listArgs:{},readTool:'shd_project_db_schema',detailKey:'table',paths:['data.tables','data'],contextRequired:true,sort:'asc'},
notes:{listTool:'shd_list_note_pages',listArgs:{limit:100},readTool:'shd_get_note_page',detailKey:'page_id',paths:['data'],sort:'desc'},
'project-overview':{listTool:'shd_get_project_context',listArgs:{include:['summary','tasks','discussions','documents','finance','terms','agents'],limit_per_section:100},paths:['data.sections.tasks','data.sections.discussions','data.sections.documents','data.sections.finance','data.sections.terms'],combinePaths:true,contextRequired:true,sort:'desc'},
files:{listTool:'shd_list_files',readTool:'shd_get_file_link',detailKey:'path',detailArgs:{download:false},detailSourceArgs:['project_code'],paths:['data'],contextRequired:true,sort:'desc'},
proposals:{listTool:'shd_list_proposals',listArgs:{limit:100},readTool:'shd_get_proposal',detailKey:'proposal_id',detailSourceArgs:[],paths:['data'],sort:'desc'},
terms:{listTool:'shd_list_terms_documents',readTool:'shd_get_terms_document',detailKey:'document_id',detailSourceArgs:['project_code'],paths:['data'],contextRequired:true,sort:'desc'},
activity:{listTool:'shd_get_realtime_events',listArgs:{module:'notifications',limit:100},paths:['data.events','events','data'],sort:'desc'},
organizations:{listTool:'shd_admin_list_organizations',listArgs:{limit:100},readTool:'shd_admin_get_organization_members',detailKey:'organization_id',detailSourceArgs:[],paths:['data'],sort:'asc'},
gitea:{listTool:'shd_list_gitea_issues',listArgs:{target:'site',limit:100},paths:['data'],sort:'desc'},
schemes:{listTool:'shd_list_scheme_documents',readTool:'shd_get_scheme_document',detailKey:'document',detailSourceArgs:['project'],paths:['data'],contextRequired:true,sort:'desc'},
'task-kanban':{listTool:'shd_list_tasks',listArgs:{limit:100},readTool:'shd_get_task',detailKey:'task_id',detailSourceArgs:[],paths:['data'],view:'kanban',groupBy:'status',sort:'asc'},
'team-workload':{listTool:'shd_get_task_workload',listArgs:{period:'month'},paths:['data.rows','data.workload','data.assignees','data'],view:'workload',sort:'desc'},
'project-timeline':{listTool:'shd_list_tasks',listArgs:{limit:100},paths:['data'],view:'timeline',sort:'asc'},
'crm-funnel':{listTool:'shd_list_crm',listArgs:{section:'deals',limit:100},readTool:'shd_get_crm_entity',detailKey:'entity_id',detailSourceArgs:['section'],paths:['data'],view:'funnel',groupBy:'stage',sort:'desc'},
'finance-dashboard':{listTool:'shd_list_finance_documents',listArgs:{limit:100},paths:['data'],view:'dashboard',sort:'desc'},
'proposal-approvals':{listTool:'shd_list_proposals',listArgs:{limit:100},readTool:'shd_get_proposal_approval',detailKey:'proposal_id',detailSourceArgs:[],paths:['data'],view:'approval',sort:'desc'},
'documents-completeness':{listTool:'shd_list_terms_documents',readTool:'shd_get_terms_document_completeness',detailKey:'document_id',detailSourceArgs:['project_code'],paths:['data'],view:'completeness',contextRequired:true,contextKeys:['project_code'],sort:'desc'},
'agents-health':{listTool:'shd_agents_overview',paths:['data.controllers','data.agents','data.user_agents','data'],view:'health',sort:'asc'},
'schedule-calendar':{listTool:'shd_get_scheduling_overview',listArgs:{limit:100},readTool:'shd_list_scheduling_bookings',detailKey:'booking_id',detailArgs:{limit:1},paths:['data.bookings','data.upcoming_bookings','data'],view:'calendar',sort:'asc'},
'acl-matrix':{listTool:'shd_list_organization_members',listArgs:{limit:100},paths:['data'],view:'matrix',contextRequired:true,contextKeys:['organization_id'],contextAll:true,sort:'asc'},
'inventory-warnings':{listTool:'shd_list_inventory_items',listArgs:{limit:100},readTool:'shd_get_inventory_item',detailKey:'item_id',detailSourceArgs:[],paths:['data'],view:'warnings',sort:'asc'},
'gitea-board':{listTool:'shd_list_gitea_issues',listArgs:{target:'site',limit:100},paths:['data'],view:'kanban',groupBy:'state',sort:'asc'},
'schemes-progress':{listTool:'shd_list_scheme_documents',readTool:'shd_get_scheme_document',detailKey:'document',detailSourceArgs:['project'],paths:['data'],view:'progress',contextRequired:true,contextKeys:['project'],sort:'desc'},
'notes-tree':{listTool:'shd_list_note_spaces',listArgs:{limit:100},readTool:'shd_get_note_space_tree',detailKey:'space_id',detailSourceArgs:[],paths:['data'],view:'tree',sort:'asc'},
'project-db-preview':{listTool:'shd_project_db_records',listArgs:{limit:100,include_fields:true,include_view_meta:true},paths:['data.records','data'],view:'preview',contextRequired:true,contextKeys:['base','project_code','table'],contextAll:true,sort:'asc'}
};
var pending=new Map(),nextId=1,rows=[],state={module:DEFAULT_MODULE,config:Object.assign({},S[DEFAULT_MODULE]),lang:'ru',busy:false};
var e={title:document.getElementById('title'),desc:document.getElementById('desc'),search:document.getElementById('search'),refresh:document.getElementById('refresh'),status:document.getElementById('status'),summary:document.getElementById('summary'),tableWrap:document.querySelector('.table-wrap'),visual:document.getElementById('visual'),rows:document.getElementById('rows'),empty:document.getElementById('empty'),details:document.getElementById('details'),detailTitle:document.getElementById('detail-title'),detailSub:document.getElementById('detail-sub'),grid:document.getElementById('grid'),close:document.getElementById('close'),ht:document.getElementById('head-title'),hs:document.getElementById('head-status'),hd:document.getElementById('head-date'),hv:document.getElementById('head-value')};
function lang(){var x=typeof window!=='undefined'&&window.openai&&window.openai.locale;x=String(x||document.documentElement.lang||'ru').toLowerCase();return x.indexOf('en')===0?'en':'ru'}
function t(k){return(C[state.lang]||C.ru)[k]||k}
function obj(x){return x&&typeof x==='object'&&!Array.isArray(x)}
function merge(a,b){return Object.assign({},a||{},b||{})}
function payload(x){if(!x)return null;if(Object.prototype.hasOwnProperty.call(x,'structuredContent'))return x.structuredContent;if(Object.prototype.hasOwnProperty.call(x,'toolOutput'))return x.toolOutput;return x}
function unwrap(x){if(x&&x.isError)throw Error(t('load'));var p=payload(x)||{};if(p.error)throw Error(String(p.error.message||p.error.detail||p.error));if(p.ok===false&&p.message)throw Error(String(p.message));return p}
function applyConfig(p){p=payload(p)||{};var m=obj(p.meta)?p.meta:{},w=obj(m.widget)?m.widget:{},mod=String(w.module||m.module||state.module||DEFAULT_MODULE);state.module=M[mod]?mod:DEFAULT_MODULE;state.config=merge(S[state.module],w);if(!obj(state.config.sourceArgs)&&obj(m.sourceArgs))state.config.sourceArgs=m.sourceArgs;state.lang=lang();labels()}
function labels(){var m=(M[state.module]||M.discussions)[state.lang]||M.discussions.ru;document.documentElement.lang=state.lang;e.title.textContent=m[0];e.desc.textContent=m[1];e.search.placeholder=t('search');e.refresh.textContent=t('refresh');e.empty.textContent=t('empty');e.close.setAttribute('aria-label',t('close'));e.ht.textContent=m[2];e.hs.textContent=t('status');e.hd.textContent=m[3];e.hv.textContent=m[4]}
function req(method,params){var o=typeof window!=='undefined'&&window.openai;if(method==='tools/call'&&o&&typeof o.callTool==='function')return Promise.resolve(o.callTool(params.name,params.arguments||{}));var id=nextId++;window.parent.postMessage({jsonrpc:'2.0',id:id,method:method,params:params||{}},'*');return new Promise(function(resolve,reject){var timeout=window.setTimeout(function(){pending.delete(id);reject(Error(t('load')))},12000);pending.set(id,{resolve:resolve,reject:reject,timeout:timeout})})}
function notify(method,params){window.parent.postMessage({jsonrpc:'2.0',method:method,params:params||{}},'*')}
function path(x,p){return String(p||'').split('.').reduce(function(v,k){return v&&typeof v==='object'?v[k]:undefined},x)}
function findArray(x,d){if(Array.isArray(x))return x;if(!obj(x)||d>2)return null;var keys=['items','records','bookings','upcoming_bookings','topics','notifications','pages','spaces','controllers','agents','user_agents','tables','monitors','data'];for(var i=0;i<keys.length;i++)if(Array.isArray(x[keys[i]]))return x[keys[i]];for(var k in x)if(Object.prototype.hasOwnProperty.call(x,k)){var a=findArray(x[k],d+1);if(a)return a}return null}
function flatten(a){var n=state.config.nested||state.config.nestedCollection;if(!n)return a;var out=[];a.forEach(function(parent){if(parent&&Array.isArray(parent[n]))parent[n].forEach(function(child){var x=Object.assign({},child);if(parent.name&&!x.group_name)x.group_name=parent.name;out.push(x)});else out.push(parent)});return out}
function list(x){var p=payload(x)||{};if(Array.isArray(p.items))return p.items;var ps=Array.isArray(state.config.paths)?state.config.paths:(Array.isArray(state.config.arrayPaths)?state.config.arrayPaths:[]),combined=[];for(var i=0;i<ps.length;i++){var c=path(p,ps[i]),a=Array.isArray(c)?c:findArray(c,0);if(a){if(state.config.combinePaths)combined=combined.concat(a);else return flatten(a)}}if(state.config.combinePaths&&combined.length)return flatten(combined);var d=p.data===undefined?p:p.data;return flatten(Array.isArray(d)?d:(findArray(d,0)||[]))}
function str(x){if(x===null||x===undefined||x==='')return'';if(typeof x==='object'){try{return JSON.stringify(x)}catch(err){return''}}return String(x)}
function id(x,i){var keys=['id','key','code','uuid','page_id','topic_id','booking_id','item_id','monitor_id','table'];for(var n=0;n<keys.length;n++)if(x&&x[keys[n]]!==undefined&&x[keys[n]]!==null&&x[keys[n]]!=='')return str(x[keys[n]]);return String(i+1)}
function title(x,i){return str(x&&(x.title||x.name||x.subject||x.display_name||x.label||x.table_name||x.table||x.key))||(t('records')+' '+(i+1))}
function date(x){return str(x&&(x.starts_at||x.start_at||x.due_date||x.checked_at||x.updated_at||x.created_at||x.sent_at||x.published_at||x.date))}
function value(x){if(!x)return'';if(x.availability_percent!==undefined&&x.availability_percent!==null)return str(x.availability_percent)+'%';if(x.quantity!==undefined&&x.quantity!==null)return str(x.quantity);if(x.record_count!==undefined&&x.record_count!==null)return str(x.record_count);return str(x.value!==undefined?x.value:(x.guest_name||x.space_name||x.group_name||x.controller_type||x.type||x.source||x.module||''))}
function sub(x){return str(x&&(x.subtitle||x.project_code||x.space_name||x.channel_name||x.group_name||x.code||x.email))}
function normalize(x,i){var y=obj(x)?Object.assign({},x):{value:x};y.id=id(x,i);y.title=title(x,i);y.subtitle=sub(x);y.status=status(x);y.date=date(x);y.value=value(x);return y}
function dateText(x){if(!x)return'—';var n=Date.parse(x);if(Number.isNaN(n))return String(x);try{return new Intl.DateTimeFormat(state.lang,{dateStyle:'medium',timeStyle:'short'}).format(new Date(n))}catch(err){return String(x)}}
function make(tag,v,cls){var n=document.createElement(tag);if(cls)n.className=cls;if(v!==undefined)n.textContent=v;return n}
function detailText(x){return x===null||x===undefined||x===''?'—':str(x)}
function renderSummary(){e.summary.replaceChildren();var n=make('span');n.append(make('strong',String(rows.length)),' '+t('records'));e.summary.append(n);var c={};rows.forEach(function(x){if(x.status)c[x.status]=(c[x.status]||0)+1});Object.keys(c).slice(0,3).forEach(function(k){var n=make('span');n.append(make('strong',String(c[k])),' '+k);e.summary.append(n)})}
function field(x,keys){for(var i=0;i<keys.length;i++){var v=x&&x[keys[i]];if(v!==undefined&&v!==null&&v!=='')return v}return''}
function numeric(x,keys){var v=field(x,keys);if(v===null||v===undefined||v==='')return null;if(typeof v==='number')return Number.isFinite(v)?v:null;var n=Number(String(v).replace(/,/g,'.').replace(/[^0-9.-]/g,''));return Number.isFinite(n)?n:null}
function groupValue(x,key){var v=key?field(x,[key]):status(x);if(obj(v))v=v.name||v.label||v.code||v.id;return str(v)||'—'}
function visualCard(x,extra){var b=make('button',undefined,'visual-card'+(extra?' '+extra:''));b.type='button';b.append(make('span',x.title||'SHD','visual-title'));if(x.subtitle)b.append(make('span',x.subtitle,'sub'));var line=make('span',undefined,'visual-line');line.append(make('span',x.status||'—','pill'));if(x.date)line.append(make('span',dateText(x.date),'visual-date'));b.append(line);var v=field(x,['total_amount','amount','planned_hours','utilization_percent','score','quantity','revision','pages','value']);if(v!==''&&v!==null)b.append(make('span',str(v),'visual-value'));b.onclick=function(){details(x)};return b}
function visualEmpty(){e.visual.replaceChildren(make('div',t('empty'),'empty'))}
function renderKanban(a){if(!a.length){visualEmpty();return}var groups={};a.forEach(function(x){var g=groupValue(x,state.config.groupBy||'status');(groups[g]||(groups[g]=[])).push(x)});var board=make('div',undefined,'kanban');Object.keys(groups).forEach(function(g){var col=make('section',undefined,'kanban-col');var head=make('div',undefined,'kanban-head');head.append(make('span',g),make('span',String(groups[g].length),'pill'));col.append(head);var list=make('div',undefined,'kanban-list');groups[g].forEach(function(x){list.append(visualCard(x))});col.append(list);board.append(col)});e.visual.append(board)}
function renderTimeline(a){if(!a.length){visualEmpty();return}var wrap=make('div',undefined,'timeline');a.slice().sort(function(x,y){return String(x.date||'').localeCompare(String(y.date||''))}).forEach(function(x){var item=make('article',undefined,'timeline-item'),rail=make('span',undefined,'timeline-rail'),dot=make('span',undefined,'timeline-dot'),content=make('div',undefined,'timeline-content');content.append(make('div',dateText(x.date),'timeline-meta'),visualCard(x,'timeline-card'));item.append(rail,dot,content);wrap.append(item)});e.visual.append(wrap)}
function renderFunnel(a){if(!a.length){visualEmpty();return}var groups={},amounts={};a.forEach(function(x){var g=groupValue(x,state.config.groupBy||'stage');groups[g]=(groups[g]||0)+1;amounts[g]=(amounts[g]||0)+(numeric(x,['total_amount','amount','value','sum'])||0)});var max=Math.max.apply(null,Object.values(groups));var wrap=make('div',undefined,'funnel');Object.keys(groups).forEach(function(g){var row=make('div',undefined,'funnel-row'),bar=document.createElement('progress');bar.className='funnel-fill';bar.max=max||1;bar.value=groups[g];row.append(make('span',g),bar,make('span',String(groups[g])+(amounts[g]?' · '+String(amounts[g]):'')));wrap.append(row)});e.visual.append(wrap)}
function metric(label,value){var d=make('div',undefined,'metric');d.append(make('span',label,'metric-label'),make('span',value===null||value===undefined?'—':str(value),'metric-value'));return d}
function renderDashboard(a,mode){var grid=make('div',undefined,'metric-grid'),total=a.length,amount=0,planned=0,capacity=0,util=0,overload=0;a.forEach(function(x){amount+=numeric(x,['total_amount','amount','total','sum','value'])||0;planned+=numeric(x,['planned_hours','planned','hours'])||0;capacity+=numeric(x,['capacity_hours','capacity'])||0;util+=numeric(x,['utilization_percent','utilization'])||0;overload+=numeric(x,['overload_hours','overload'])||0});grid.append(metric(t('total'),total));if(mode==='workload'){grid.append(metric(t('planned'),planned||null),metric(t('capacity'),capacity||null),metric(t('utilization'),a.length?Math.round(util/a.length)+'%':null),metric(t('overload'),overload||0))}else{grid.append(metric(t('amount'),amount||null),metric(t('status'),Object.keys(a.reduce(function(m,x){m[groupValue(x,'status')]=1;return m},{})).length))}e.visual.append(grid);if(!a.length){visualEmpty();return}var list=make('div',undefined,'kanban-list');a.slice(0,60).forEach(function(x){list.append(visualCard(x))});e.visual.append(list)}
function approvalValue(x){return groupValue(x,'approval_status')!=='—'?groupValue(x,'approval_status'):groupValue(x,'approval_state')!=='—'?groupValue(x,'approval_state'):groupValue(x,'status')}
function renderStatusSummary(a,keys){if(!a.length){visualEmpty();return}var counts={};a.forEach(function(x){var g=groupValue(x,keys[0]);if(g==='—')g=groupValue(x,keys[1]);if(g==='—')g=groupValue(x,'status');counts[g]=(counts[g]||0)+1});var grid=make('div',undefined,'metric-grid');Object.keys(counts).slice(0,6).forEach(function(g){grid.append(metric(g,counts[g]))});e.visual.append(grid);var list=make('div',undefined,'kanban-list');a.slice(0,60).forEach(function(x){list.append(visualCard(x))});e.visual.append(list)}
function progressValue(x){var n=numeric(x,['score','completeness_score','completion_percent','progress','availability_percent']);if(n!==null)return Math.max(0,Math.min(100,n));if(x.complete===true||x.is_complete===true||String(status(x)).toLowerCase()==='complete')return 100;return 0}
function renderProgress(a){if(!a.length){visualEmpty();return}var grid=make('div',undefined,'preview-grid');a.forEach(function(x){var card=make('article',undefined,'progress-card');card.append(make('strong',x.title||'SHD','visual-title'));var meta=make('span',x.subtitle||dateText(x.date),'visual-date');card.append(meta);var p=document.createElement('progress');p.className='progress';p.max=100;p.value=progressValue(x);card.append(p,make('span',t('progress')+': '+String(p.value)+'%','visual-value'));card.onclick=function(){details(x)};grid.append(card)});e.visual.append(grid)}
function isWarning(x){var s=String(status(x)).toLowerCase();if(x.low_stock||x.is_low_stock||x.critical||x.warning)return true;if(/low|critical|out|error|overdue|maintenance/.test(s))return true;var q=numeric(x,['quantity','available_quantity','stock']);var min=numeric(x,['min_quantity','minimum_quantity','reorder_level']);return q!==null&&min!==null&&q<=min}
function renderWarnings(a){var warnings=a.filter(isWarning),grid=make('div',undefined,'metric-grid');grid.append(metric(t('warnings'),warnings.length),metric(t('total'),a.length));e.visual.append(grid);if(!warnings.length){e.visual.append(make('div',t('noWarnings'),'empty'));return}var list=make('div',undefined,'kanban-list');warnings.forEach(function(x){list.append(visualCard(x,'warning'))});e.visual.append(list)}
function renderCalendar(a){if(!a.length){visualEmpty();return}var days={};a.forEach(function(x){var d=x.date?String(x.date).slice(0,10):'—';(days[d]||(days[d]=[])).push(x)});var cal=make('div',undefined,'calendar');Object.keys(days).forEach(function(d){var day=make('section',undefined,'calendar-day');day.append(make('div',d,'calendar-head'));days[d].forEach(function(x){day.append(visualCard(x))});cal.append(day)});e.visual.append(cal)}
function memberName(x){var v=field(x,['member_name','username','email','name','display_name','user']);if(obj(v))return str(v.name||v.username||v.email||v.id);return str(v)||x.id}
function permissionText(x){var p=x.permissions||x.permission||x.scopes||'';if(obj(p))return Object.keys(p).filter(function(k){return p[k]===true||p[k]===1||typeof p[k]==='string'}).map(function(k){return k+(typeof p[k]==='string'?'='+p[k]:'')}).join(', ')||'—';if(Array.isArray(p))return p.join(', ')||'—';return str(p)||'—'}
function renderMatrix(a){if(!a.length){visualEmpty();return}var wrap=make('div',undefined,'matrix-wrap'),table=make('table',undefined,'matrix-table'),head=make('tr');[t('member'),t('role'),t('status'),t('permissions')].forEach(function(x){head.append(make('th',x))});table.append(make('thead').appendChild(head).parentNode);var body=make('tbody');a.forEach(function(x){var row=make('tr');row.append(make('td',memberName(x)),make('td',str(field(x,['role','organization_role','type']))||'—'),make('td',status(x)||'—'),make('td',permissionText(x)));row.onclick=function(){details(x)};row.className='record';body.append(row)});table.append(body);wrap.append(table);e.visual.append(wrap)}
function treeId(x,i){return str(field(x,['id','space_id','page_id','collection_id','key']))||String(i)}
function treeParent(x){return str(field(x,['parent_id','parentId','parent_space_id','parent_collection_id']))}
function renderTree(a){if(!a.length){visualEmpty();return}var by={};a.forEach(function(x,i){var p=treeParent(x);(by[p]||(by[p]=[])).push({x:x,i:i})});var tree=make('div',undefined,'tree');function branch(parent,host,seen){(by[parent]||[]).forEach(function(entry){var id=treeId(entry.x,entry.i);if(seen[id])return;var wrap=make('div',undefined,'tree-node');wrap.append(visualCard(entry.x));host.append(wrap);var child=make('div',undefined,'tree-children');branch(id,child,Object.assign({},seen,(function(){var o={};o[id]=true;return o})()));if(child.childNodes.length)wrap.append(child)})}branch('',tree,{});if(!tree.childNodes.length)a.forEach(function(x){tree.append(visualCard(x))});e.visual.append(tree)}
function renderPreview(a){if(!a.length){visualEmpty();return}var grid=make('div',undefined,'preview-grid');a.forEach(function(x){var card=make('article',undefined,'preview-card');card.append(make('strong',x.title||'SHD','visual-title'));var fields=make('div',undefined,'preview-fields');Object.keys(x).filter(function(k){return['id','title','subtitle','status','date','value','meta'].indexOf(k)===-1}).slice(0,10).forEach(function(k){var cell=make('div',undefined,'preview-field');cell.append(make('strong',k+': '),make('span',str(x[k])));fields.append(cell)});card.append(fields);card.onclick=function(){details(x)};grid.append(card)});e.visual.append(grid)}
function renderVisual(a){e.visual.replaceChildren();var v=state.config.view||'table';if(v==='kanban')renderKanban(a);else if(v==='workload'||v==='dashboard')renderDashboard(a,v);else if(v==='timeline')renderTimeline(a);else if(v==='funnel')renderFunnel(a);else if(v==='approval')renderStatusSummary(a,['approval_status','approval_state']);else if(v==='completeness'||v==='progress')renderProgress(a);else if(v==='health')renderStatusSummary(a,['health','status']);else if(v==='calendar')renderCalendar(a);else if(v==='matrix')renderMatrix(a);else if(v==='warnings')renderWarnings(a);else if(v==='tree')renderTree(a);else if(v==='preview')renderPreview(a);else renderDashboard(a,'dashboard')}
function renderTable(a){e.rows.replaceChildren();e.empty.hidden=a.length!==0;a.forEach(function(x){var r=document.createElement('tr');r.className='record';r.tabIndex=0;r.setAttribute('role','button');r.setAttribute('aria-label',x.title);var n=make('td');n.append(make('span',x.title,'title'));if(x.subtitle)n.append(make('span',x.subtitle,'sub'));var s=make('span',x.status||'—','pill');if(x.status)s.classList.add(String(x.status).toLowerCase().replace(/[^a-z0-9_-]+/g,'-'));var sc=make('td');sc.append(s);r.append(n,sc,make('td',dateText(x.date)),make('td',x.value||'—'));r.onclick=function(){details(x)};r.onkeydown=function(ev){if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();details(x)}};e.rows.append(r)});renderSummary()}
function renderRows(){var q=e.search.value.trim().toLowerCase(),a=rows.filter(function(x){return!q||[x.title,x.subtitle,x.status,x.id,x.value].map(str).join(' ').toLowerCase().indexOf(q)!==-1});if(state.config.view&&state.config.view!=='table'){e.tableWrap.hidden=true;e.visual.hidden=false;e.empty.hidden=true;renderVisual(a);renderSummary();return}e.tableWrap.hidden=false;e.visual.hidden=true;renderTable(a)}
function render(x){var p=unwrap(x);applyConfig(p);rows=list(p).map(normalize);var dir=state.config.sort==='asc'?1:-1;rows.sort(function(a,b){var x=Date.parse(a.date),y=Date.parse(b.date);if(!Number.isNaN(x)&&!Number.isNaN(y)&&x!==y)return(x-y)*dir;return a.title.localeCompare(b.title,state.lang)*dir});renderRows()}
function setStatus(x,bad){e.status.textContent=x||'';e.status.className=bad?'status error':'status'}
function add(k,v){var d=make('div',undefined,'detail');d.append(make('span',k,'label'),make('span',detailText(v),'value'));e.grid.append(d)}
function show(x){e.detailTitle.textContent=x.title||'SHD';e.detailSub.textContent=x.subtitle||x.id||'';e.grid.replaceChildren();add(t('id'),x.id);if(x.status)add(t('status'),x.status);if(x.date)add(t('date'),dateText(x.date));if(x.value)add(t('value'),x.value);Object.keys(x).filter(function(k){return['id','title','subtitle','status','date','value','meta'].indexOf(k)===-1}).slice(0,8).forEach(function(k){add(k,x[k])});e.details.hidden=false}
function details(x){show(x);if(!state.config.readTool)return;var k=state.config.detailKey;if(!k)return;var v=x[k]===undefined?x.id:x[k];if(v===undefined||v===null||v==='')return;if(state.module==='status-page'&&!/^\d+$/.test(String(v)))return;var a={};if(Array.isArray(state.config.detailSourceArgs)){var ctx=merge(state.config.listArgs,state.config.sourceArgs);state.config.detailSourceArgs.forEach(function(name){if(ctx[name]!==undefined)a[name]=ctx[name]})}else a=merge(state.config.sourceArgs,state.config.detailArgs);a=merge(a,state.config.detailArgs);a[k]=k==='table'?String(v):(Number(v)||v);req('tools/call',{name:state.config.readTool,arguments:a}).then(function(result){var p=unwrap(result),d=p.data===undefined?p:p.data;e.grid.replaceChildren();if(Array.isArray(d)){add(t('records'),d.length);d.slice(0,6).forEach(function(q,i){add(String(i+1),q&&(q.message||q.body||q.content||q.title||q))})}else if(obj(d))Object.keys(d).slice(0,12).forEach(function(q){add(q,d[q])});else add(t('details'),d)}).catch(function(err){setStatus(err.message||t('detail'),true)})}
function contextReady(a){if(!state.config.contextRequired)return true;if(Array.isArray(state.config.contextSets)&&state.config.contextSets.length)return state.config.contextSets.every(function(set){return set.some(function(k){return a[k]!==undefined&&a[k]!==null&&a[k]!==''})});var keys=Array.isArray(state.config.contextKeys)&&state.config.contextKeys.length?state.config.contextKeys:['base','project_code','project'];var ready=state.config.contextAll?keys.every(function(k){return a[k]!==undefined&&a[k]!==null&&a[k]!==''}):keys.some(function(k){return a[k]!==undefined&&a[k]!==null&&a[k]!==''});return ready}
function refresh(){if(state.busy)return;var a=merge(state.config.listArgs,state.config.sourceArgs);if(!contextReady(a)){setStatus(t('context'),true);return}if(!state.config.listTool)return;state.busy=true;e.refresh.disabled=true;setStatus(t('loading'),false);req('tools/call',{name:state.config.listTool,arguments:a}).then(function(x){render(x);setStatus('',false)}).catch(function(err){setStatus(err.message||t('load'),true)}).finally(function(){state.busy=false;e.refresh.disabled=false})}
function init(){return req('ui/initialize',{protocolVersion:'2025-11-25',appInfo:{name:'SHD '+(M[state.module]||M.discussions).ru[0],title:'SHD MCP',version:'1.0.0',websiteUrl:'https://shd.xyz.su'},appCapabilities:{}}).then(function(){notify('ui/notifications/initialized',{})}).catch(function(){})}
window.addEventListener('message',function(ev){if(ev.source!==window.parent)return;var m=ev.data;if(!m||m.jsonrpc!=='2.0')return;if(m.id!==undefined&&pending.has(m.id)){var q=pending.get(m.id);pending.delete(m.id);window.clearTimeout(q.timeout);if(m.error)q.reject(Error(m.error.message||t('load')));else q.resolve(m.result);return}if(m.method==='ui/notifications/tool-input'&&!rows.length){try{render(m.params||{})}catch(err){setStatus(err.message||t('load'),true)}}if(m.method==='ui/notifications/tool-result'){try{render(m.params&&m.params.structuredContent)}catch(err){setStatus(err.message||t('load'),true)}}},{passive:true});
e.search.oninput=renderRows;e.refresh.onclick=refresh;e.close.onclick=function(){e.details.hidden=true};state.lang=lang();labels();void init();var out=typeof window!=='undefined'&&window.openai?window.openai.toolOutput:null;if(out){try{render(out)}catch(err){setStatus(err.message||t('load'),true)}}else{var input=typeof window!=='undefined'&&window.openai?window.openai.toolInput:null;if(input){try{render(input)}catch(err){setStatus(err.message||t('load'),true)}}else refresh()}}
}());
</script>
<script data-shd-widget-write-actions="js">
(function () {
'use strict';
if (window.__SHD_WIDGET_WRITE_ACTIONS__) return;
window.__SHD_WIDGET_WRITE_ACTIONS__ = true;
var copy = {
ru: {
actions: 'Действия',
chooseAction: 'Выберите действие',
selectRecord: 'Сначала выберите запись.',
submit: 'Сохранить',
cancel: 'Отмена',
confirm: 'Подтверждаю действие',
warning: 'Проверьте данные перед записью.',
destructive: 'Операция может изменить или удалить данные.',
required: 'Заполните обязательное поле.',
invalidJson: 'Введите корректный JSON.',
success: 'Изменения сохранены.',
failure: 'Не удалось сохранить изменения.',
loading: 'Сохраняю…',
noActions: 'Для этого виджета нет доступных действий.',
refreshed: 'Список обновлён.',
fieldValue: 'Значение',
},
en: {
actions: 'Actions',
chooseAction: 'Choose an action',
selectRecord: 'Select a record first.',
submit: 'Save',
cancel: 'Cancel',
confirm: 'I confirm this action',
warning: 'Review the values before writing.',
destructive: 'This operation may change or remove data.',
required: 'Fill in the required field.',
invalidJson: 'Enter valid JSON.',
success: 'Changes saved.',
failure: 'Could not save changes.',
loading: 'Saving…',
noActions: 'No actions are available for this widget.',
refreshed: 'List refreshed.',
fieldValue: 'Value',
},
};
var state = {
widget: {},
records: [],
selected: null,
action: null,
inputs: {},
busy: false,
lang: String(document.documentElement.lang || 'ru').toLowerCase().indexOf('ru') === 0 ? 'ru' : 'en',
};
var pending = new Map();
var nextRequestId = 1;
var elements = {};
function t(key) {
return (copy[state.lang] && copy[state.lang][key]) || copy.en[key] || key;
}
function object(value) {
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
}
function normalize(value) {
if (!value) return null;
if (value.structuredContent) return normalize(value.structuredContent);
if (value.toolOutput) return normalize(value.toolOutput);
if (value.toolInput) return normalize(value.toolInput);
if (value.arguments && !value.data && !value.items && !value.projects) return normalize(value.arguments);
return value;
}
function widgetFrom(value) {
var payload = normalize(value) || {};
var meta = object(payload.meta) ? payload.meta : {};
return object(meta.widget) ? meta.widget : {};
}
function recordsFrom(value) {
var payload = normalize(value) || {};
var data = Array.isArray(payload.items) ? payload.items : payload.projects;
if (!Array.isArray(data)) data = payload.data;
if (data && !Array.isArray(data) && Array.isArray(data.data)) data = data.data;
return Array.isArray(data) ? data.filter(function (item) { return object(item); }) : [];
}
function readPath(value, path) {
if (!value || path === undefined || path === null) return undefined;
var parts = String(path).split('.');
var current = value;
for (var index = 0; index < parts.length; index += 1) {
if (current === undefined || current === null) return undefined;
current = current[parts[index]];
}
return current;
}
function firstValue(paths) {
var list = Array.isArray(paths) ? paths : [paths];
var sources = [state.selected, state.widget.sourceArgs, state.widget.listArgs];
for (var sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
var sourceObject = sources[sourceIndex];
if (!sourceObject) continue;
for (var pathIndex = 0; pathIndex < list.length; pathIndex += 1) {
var value = readPath(sourceObject, list[pathIndex]);
if (value !== undefined && value !== null && value !== '') return value;
}
}
return undefined;
}
function text(value) {
if (value === undefined || value === null) return '';
if (typeof value === 'object') return JSON.stringify(value);
return String(value);
}
function label(value) {
if (object(value)) return value[state.lang] || value.ru || value.en || t('fieldValue');
return String(value || t('fieldValue'));
}
function actionLabel(action) {
return label(action && action.label) || action.id || action.tool || t('actions');
}
function hasTargets(action) {
return Boolean(action && ((Array.isArray(action.targets) && action.targets.length) || action.target));
}
function actionList() {
return Array.isArray(state.widget.actions) ? state.widget.actions.filter(function (action) {
return action && action.tool;
}) : [];
}
function request(method, params) {
var openai = typeof window !== 'undefined' ? window.openai : undefined;
if (method === 'tools/call' && openai && typeof openai.callTool === 'function') {
return openai.callTool(params.name, params.arguments || {});
}
var id = nextRequestId++;
window.parent.postMessage({ jsonrpc: '2.0', id: id, method: method, params: params || {} }, '*');
return new Promise(function (resolve, reject) {
var timeout = window.setTimeout(function () {
pending.delete(id);
reject(new Error(t('failure')));
}, 15000);
pending.set(id, { resolve: resolve, reject: reject, timeout: timeout });
});
}
function unwrap(value) {
var payload = normalize(value);
if (value && value.isError) throw new Error(payload && payload.error ? text(payload.error) : t('failure'));
if (payload && payload.error) throw new Error(text(payload.error));
return payload || value;
}
function make(tag, value, className) {
var node = document.createElement(tag);
if (className) node.className = className;
if (value !== undefined) node.textContent = value;
return node;
}
function ensurePanel() {
if (elements.panel) return;
var root = document.querySelector('main') || document.body;
var panel = make('section', undefined, 'shd-widget-actions');
panel.hidden = true;
panel.setAttribute('aria-live', 'polite');
var header = make('div', undefined, 'shd-widget-actions-header');
header.append(make('strong', t('actions')), make('span', '', 'shd-widget-actions-selection'));
var select = document.createElement('select');
select.className = 'shd-widget-actions-select';
select.setAttribute('aria-label', t('actions'));
var form = make('form', undefined, 'shd-widget-actions-form');
var status = make('div', '', 'shd-widget-actions-status');
var buttons = make('div', undefined, 'shd-widget-actions-buttons');
var submit = make('button', t('submit'), 'shd-widget-actions-submit');
submit.type = 'submit';
var cancel = make('button', t('cancel'), 'shd-widget-actions-cancel');
cancel.type = 'button';
buttons.append(submit, cancel);
panel.append(header, select, form, status, buttons);
root.append(panel);
elements = { panel: panel, selection: header.lastChild, select: select, form: form, status: status, submit: submit, cancel: cancel };
select.addEventListener('change', function () {
var selected = actionList().find(function (action) { return action.id === select.value; });
state.action = selected || null;
renderForm();
});
cancel.addEventListener('click', function () {
state.action = null;
select.value = '';
renderForm();
});
form.addEventListener('submit', function (event) {
event.preventDefault();
submitAction();
});
}
function inputValue(fieldConfig) {
var value = firstValue(fieldConfig.source || []);
if (value === undefined && fieldConfig.default !== undefined) value = fieldConfig.default;
return value;
}
function createInput(fieldConfig) {
var type = fieldConfig.type || 'text';
var input;
if (type === 'textarea' || type === 'json') {
input = document.createElement('textarea');
input.rows = type === 'json' ? 4 : 3;
if (type === 'json') input.placeholder = '{ }';
} else if (type === 'select') {
input = document.createElement('select');
(fieldConfig.options || []).forEach(function (option) {
var item = document.createElement('option');
item.value = String(option[0]);
item.textContent = state.lang === 'ru' ? String(option[1]) : String(option[2] || option[1]);
input.append(item);
});
} else {
input = document.createElement('input');
input.type = type === 'number' || type === 'email' ? type : 'text';
}
input.className = 'shd-widget-actions-input';
input.name = fieldConfig.name;
var value = inputValue(fieldConfig);
if (type === 'checkbox') {
input.type = 'checkbox';
input.checked = value === true || value === 'true' || value === 1 || value === '1';
} else if (value !== undefined && value !== null) {
input.value = type === 'json' && typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value);
}
if (fieldConfig.required) input.required = true;
return input;
}
function renderForm() {
ensurePanel();
var selectedActionId = state.action ? state.action.id : '';
elements.select.replaceChildren();
var placeholder = document.createElement('option');
placeholder.value = '';
placeholder.textContent = t('chooseAction');
elements.select.append(placeholder);
actionList().forEach(function (action) {
var option = document.createElement('option');
option.value = action.id;
option.textContent = actionLabel(action);
elements.select.append(option);
});
elements.select.value = selectedActionId;
elements.select.hidden = actionList().length === 0;
elements.form.replaceChildren();
elements.status.textContent = '';
elements.submit.disabled = state.busy;
var action = state.action;
if (!action) {
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
elements.submit.hidden = true;
elements.cancel.hidden = true;
if (actionList().length && !state.selected && actionList().every(hasTargets)) {
elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
}
return;
}
elements.selection.textContent = state.selected ? text(state.selected.title || state.selected.name || state.selected.id || '') : '';
elements.submit.hidden = false;
elements.cancel.hidden = false;
if (hasTargets(action) && !state.selected) elements.form.append(make('div', t('selectRecord'), 'shd-widget-actions-hint'));
(action.fields || []).forEach(function (fieldConfig) {
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
var wrapper = make('label', undefined, 'shd-widget-actions-field');
wrapper.append(make('span', label(fieldConfig.label)));
wrapper.append(createInput(fieldConfig));
elements.form.append(wrapper);
});
if (action.confirm) {
var confirmLabel = make('label', undefined, 'shd-widget-actions-confirm');
var confirm = document.createElement('input');
confirm.type = 'checkbox';
confirm.name = '__confirm';
confirm.required = true;
confirmLabel.append(confirm, make('span', t('confirm')));
elements.form.append(confirmLabel);
}
var notice = make('div', action.destructive ? t('destructive') : t('warning'), action.destructive ? 'shd-widget-actions-warning shd-widget-actions-danger' : 'shd-widget-actions-warning');
elements.form.append(notice);
}
function coerce(value, type) {
if (type === 'number') {
var number = Number(value);
return Number.isFinite(number) ? number : value;
}
if (type === 'checkbox') return Boolean(value);
if (type === 'json') {
try { return JSON.parse(value); } catch (error) { throw new Error(t('invalidJson')); }
}
return value;
}
function targetType(name) {
return /^(task|document|contract|booking|item|monitor|proposal|approval|page|notification|topic|membership|organization|space|status|project)_?id$/.test(name) || name === 'issue_number' || name === 'number' ? 'number' : 'text';
}
function targetList(action) {
if (Array.isArray(action.targets)) return action.targets;
return action.target ? [action.target] : [];
}
function collectArguments(action) {
var args = Object.assign({}, action.fixed || {});
(action.context || []).forEach(function (name) {
var value = firstValue([name]);
if (value !== undefined && value !== null && value !== '') args[name] = value;
});
targetList(action).forEach(function (targetConfig) {
var value = firstValue(targetConfig.source || targetConfig.name);
if (value === undefined || value === null || value === '') throw new Error(t('selectRecord'));
args[targetConfig.name] = coerce(value, targetConfig.type || targetType(targetConfig.name));
});
(action.fields || []).forEach(function (fieldConfig) {
if (!fieldConfig || !fieldConfig.name || fieldConfig.generated || fieldConfig.type === 'hidden') return;
var input = Array.prototype.find.call(elements.form.querySelectorAll('[name]'), function (node) {
return node.getAttribute('name') === fieldConfig.name;
});
if (!input) return;
var raw = fieldConfig.type === 'checkbox' ? input.checked : input.value.trim();
if (raw === '' && !input.checked && !fieldConfig.required) return;
if (raw === '' && fieldConfig.required) throw new Error(t('required'));
args[fieldConfig.name] = coerce(raw, fieldConfig.type || 'text');
});
if (action.idempotencyKey) args.idempotency_key = 'shd-widget-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10);
if (action.confirmArg) args.confirm = true;
return args;
}
function setStatus(message, error) {
elements.status.textContent = message || '';
elements.status.className = 'shd-widget-actions-status' + (error ? ' shd-widget-actions-status-error' : '');
}
function submitAction() {
if (state.busy || !state.action) return;
var action = state.action;
var args;
try { args = collectArguments(action); } catch (error) { setStatus(error.message || t('failure'), true); return; }
state.busy = true;
elements.submit.disabled = true;
elements.cancel.disabled = true;
setStatus(t('loading'), false);
request('tools/call', { name: action.tool, arguments: args }).then(function (result) {
unwrap(result);
setStatus(t('success'), false);
var refresh = document.getElementById('refresh');
if (refresh && typeof refresh.click === 'function') window.setTimeout(function () { refresh.click(); }, 250);
}).catch(function (error) {
setStatus(error && error.message ? error.message : t('failure'), true);
}).finally(function () {
state.busy = false;
elements.submit.disabled = false;
elements.cancel.disabled = false;
});
}
function recordFromNode(node) {
var content = String(node.textContent || '').toLowerCase();
var exact = state.records.find(function (item) {
var id = text(item.id || item.code || item.key || item.number).toLowerCase();
return id && content.indexOf(id) !== -1;
});
if (exact) return exact;
return state.records.find(function (item) {
var title = text(item.title || item.name || item.display_name || item.number).toLowerCase();
return title && content.indexOf(title) !== -1;
}) || null;
}
function update(value) {
var payload = normalize(value) || {};
var widget = widgetFrom(payload);
if (Object.keys(widget).length) state.widget = widget;
var records = recordsFrom(payload);
if (records.length || Array.isArray(payload.data) || Array.isArray(payload.items) || Array.isArray(payload.projects)) state.records = records;
ensurePanel();
var actions = actionList();
elements.panel.hidden = actions.length === 0;
if (actions.length && !state.action) renderForm();
else if (actions.length) renderForm();
}
document.addEventListener('click', function (event) {
var node = event.target && event.target.closest ? event.target.closest('.record,.project-row,.visual-card,.progress-card,.preview-card,.calendar-card,.timeline-card,.kanban-card') : null;
if (!node || (elements.panel && elements.panel.contains(node))) return;
var item = recordFromNode(node);
if (!item) return;
state.selected = item;
ensurePanel();
renderForm();
}, true);
window.addEventListener('message', function (event) {
if (event.source !== window.parent) return;
var message = event.data;
if (!message || message.jsonrpc !== '2.0') return;
if (message.id !== undefined && pending.has(message.id)) {
var requestState = pending.get(message.id);
pending.delete(message.id);
window.clearTimeout(requestState.timeout);
if (message.error) requestState.reject(new Error(message.error.message || t('failure')));
else requestState.resolve(message.result);
return;
}
if (message.method === 'ui/notifications/tool-input' || message.method === 'ui/notifications/tool-result') update(message.params || {});
}, { passive: true });
ensurePanel();
var initial = typeof window !== 'undefined' && window.openai ? (window.openai.toolOutput || window.openai.toolInput) : null;
if (initial) update(initial);
}());
</script>
</body>
</html>
@@ -0,0 +1,20 @@
{
"schemaVersion": 1,
"name": "shd-agents-health",
"version": "1.0.0",
"resources": [
{
"file": "index.html",
"bytes": 69113,
"gzipBytes": 18846,
"mediaType": "text/html;profile=mcp-app",
"uri": "ui://shd/agents-health/v1.html",
"sha256": "d7042f24d6bce4b94db2726c252def333bb99c619295c85a8fb4855788c35373"
}
],
"license": "../../../../../LICENSE",
"source": {
"repository": "https://github.com/bulava92/shd-mcp-plugin",
"path": "plugins/shd-mcp-plugin/widgets/agents-health/v1/index.html"
}
}

Some files were not shown because too many files have changed in this diff Show More