Publish SHD MCP plugin package
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.DS_Store
|
||||
.idea/
|
||||
.vscode/
|
||||
*.log
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
@@ -0,0 +1,93 @@
|
||||
# SHD MCP in ChatGPT
|
||||
|
||||
This file describes the user-side connection. The repository cannot create an
|
||||
app inside another person’s 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.
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"shd": {
|
||||
"type": "http",
|
||||
"url": "https://shd.xyz.su/mcp",
|
||||
"oauth_resource": "https://shd.xyz.su/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 record’s 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
Reference in New Issue
Block a user