feat: phase 1 workspace isolation across auth and core APIs (#112)
* feat: add workspace-scoped auth sessions and core API filtering * feat: extend workspace scoping to search status standup and messaging * feat: scope agent connect github and alert workflows by workspace * fix: scope status sync and session alerts by workspace * feat: add phase2 workspace migration and scope chat pipeline alerts * feat: add model selection for agents and cron jobs * feat: add deterministic agent avatars to task and squad views * feat: add read-only cron calendar and agenda views * feat: render task descriptions with markdown
This commit is contained in:
parent
d682395266
commit
498cb2f8d5
|
|
@ -29,8 +29,10 @@
|
|||
"postcss": "^8.5.2",
|
||||
"react": "^19.0.1",
|
||||
"react-dom": "^19.0.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"reactflow": "^11.11.4",
|
||||
"recharts": "^3.7.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwind-merge": "^3.4.0",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
|
|
|
|||
183
pnpm-lock.yaml
183
pnpm-lock.yaml
|
|
@ -47,12 +47,18 @@ importers:
|
|||
react-dom:
|
||||
specifier: ^19.0.1
|
||||
version: 19.2.4(react@19.2.4)
|
||||
react-markdown:
|
||||
specifier: ^10.1.0
|
||||
version: 10.1.0(@types/react@19.2.13)(react@19.2.4)
|
||||
reactflow:
|
||||
specifier: ^11.11.4
|
||||
version: 11.11.4(@types/react@19.2.13)(immer@11.1.3)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
recharts:
|
||||
specifier: ^3.7.0
|
||||
version: 3.7.0(@types/react@19.2.13)(react-dom@19.2.4(react@19.2.4))(react-is@17.0.2)(react@19.2.4)(redux@5.0.1)
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
tailwind-merge:
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0
|
||||
|
|
@ -1286,6 +1292,9 @@ packages:
|
|||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
'@types/estree-jsx@1.0.5':
|
||||
resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==}
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
|
|
@ -1321,6 +1330,9 @@ packages:
|
|||
'@types/react@19.2.13':
|
||||
resolution: {integrity: sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==}
|
||||
|
||||
'@types/unist@2.0.11':
|
||||
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||
|
||||
'@types/unist@3.0.3':
|
||||
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||
|
||||
|
|
@ -1890,6 +1902,9 @@ packages:
|
|||
character-entities@2.0.2:
|
||||
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
||||
|
||||
character-reference-invalid@2.0.1:
|
||||
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
||||
|
||||
check-error@2.1.3:
|
||||
resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==}
|
||||
engines: {node: '>= 16'}
|
||||
|
|
@ -2331,6 +2346,9 @@ packages:
|
|||
resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
estree-util-is-identifier-name@3.0.0:
|
||||
resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==}
|
||||
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
|
|
@ -2590,6 +2608,9 @@ packages:
|
|||
hast-util-to-html@9.0.5:
|
||||
resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==}
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==}
|
||||
|
||||
hast-util-to-parse5@8.0.1:
|
||||
resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==}
|
||||
|
||||
|
|
@ -2625,6 +2646,9 @@ packages:
|
|||
resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
html-url-attributes@3.0.1:
|
||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||
|
||||
html-void-elements@3.0.0:
|
||||
resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
|
||||
|
||||
|
|
@ -2682,6 +2706,9 @@ packages:
|
|||
ini@1.3.8:
|
||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||
|
||||
inline-style-parser@0.2.7:
|
||||
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
|
||||
|
||||
internal-slot@1.1.0:
|
||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -2694,6 +2721,12 @@ packages:
|
|||
resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
is-alphabetical@2.0.1:
|
||||
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||
|
||||
is-alphanumerical@2.0.1:
|
||||
resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
|
||||
|
||||
is-array-buffer@3.0.5:
|
||||
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -2733,6 +2766,9 @@ packages:
|
|||
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-decimal@2.0.1:
|
||||
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -2749,6 +2785,9 @@ packages:
|
|||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-hexadecimal@2.0.1:
|
||||
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
||||
|
||||
is-identifier@1.0.1:
|
||||
resolution: {integrity: sha512-HQ5v4rEJ7REUV54bCd2l5FaD299SGDEn2UPoVXaTHAyGviLq2menVUD2udi3trQ32uvB6LdAh/0ck2EuizrtpA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -2990,6 +3029,15 @@ packages:
|
|||
mdast-util-gfm@3.1.0:
|
||||
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
|
||||
|
||||
mdast-util-mdx-expression@2.0.1:
|
||||
resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
|
||||
|
||||
mdast-util-mdx-jsx@3.2.0:
|
||||
resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==}
|
||||
|
||||
mdast-util-mdxjs-esm@2.0.1:
|
||||
resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==}
|
||||
|
||||
mdast-util-phrasing@4.1.0:
|
||||
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
|
||||
|
||||
|
|
@ -3262,6 +3310,9 @@ packages:
|
|||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parse-entities@4.0.2:
|
||||
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||
|
||||
parse-ms@4.0.0:
|
||||
resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3451,6 +3502,12 @@ packages:
|
|||
react-is@17.0.2:
|
||||
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
|
||||
|
||||
react-markdown@10.1.0:
|
||||
resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '>=18'
|
||||
react: '>=18'
|
||||
|
||||
react-redux@9.2.0:
|
||||
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
|
||||
peerDependencies:
|
||||
|
|
@ -3777,6 +3834,12 @@ packages:
|
|||
style-mod@4.1.3:
|
||||
resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
|
||||
|
||||
style-to-js@1.1.21:
|
||||
resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==}
|
||||
|
||||
style-to-object@1.0.14:
|
||||
resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==}
|
||||
|
||||
styled-jsx@5.1.6:
|
||||
resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
|
|
@ -5702,6 +5765,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
||||
'@types/estree-jsx@1.0.5':
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
'@types/geojson@7946.0.16': {}
|
||||
|
|
@ -5734,6 +5801,8 @@ snapshots:
|
|||
dependencies:
|
||||
csstype: 3.2.3
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@types/use-sync-external-store@0.0.6': {}
|
||||
|
|
@ -6336,6 +6405,8 @@ snapshots:
|
|||
|
||||
character-entities@2.0.2: {}
|
||||
|
||||
character-reference-invalid@2.0.1: {}
|
||||
|
||||
check-error@2.1.3: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
|
|
@ -6914,6 +6985,8 @@ snapshots:
|
|||
|
||||
estraverse@5.3.0: {}
|
||||
|
||||
estree-util-is-identifier-name@3.0.0: {}
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
|
|
@ -7211,6 +7284,26 @@ snapshots:
|
|||
stringify-entities: 4.0.4
|
||||
zwitch: 2.0.4
|
||||
|
||||
hast-util-to-jsx-runtime@2.3.6:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
'@types/hast': 3.0.4
|
||||
'@types/unist': 3.0.3
|
||||
comma-separated-tokens: 2.0.3
|
||||
devlop: 1.1.0
|
||||
estree-util-is-identifier-name: 3.0.0
|
||||
hast-util-whitespace: 3.0.0
|
||||
mdast-util-mdx-expression: 2.0.1
|
||||
mdast-util-mdx-jsx: 3.2.0
|
||||
mdast-util-mdxjs-esm: 2.0.1
|
||||
property-information: 7.1.0
|
||||
space-separated-tokens: 2.0.2
|
||||
style-to-js: 1.1.21
|
||||
unist-util-position: 5.0.0
|
||||
vfile-message: 4.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
hast-util-to-parse5@8.0.1:
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
|
|
@ -7258,6 +7351,8 @@ snapshots:
|
|||
dependencies:
|
||||
whatwg-encoding: 3.1.1
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
||||
html-void-elements@3.0.0: {}
|
||||
|
||||
html-whitespace-sensitive-tag-names@3.0.1: {}
|
||||
|
|
@ -7307,6 +7402,8 @@ snapshots:
|
|||
|
||||
ini@1.3.8: {}
|
||||
|
||||
inline-style-parser@0.2.7: {}
|
||||
|
||||
internal-slot@1.1.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
|
@ -7317,6 +7414,13 @@ snapshots:
|
|||
|
||||
is-absolute-url@4.0.1: {}
|
||||
|
||||
is-alphabetical@2.0.1: {}
|
||||
|
||||
is-alphanumerical@2.0.1:
|
||||
dependencies:
|
||||
is-alphabetical: 2.0.1
|
||||
is-decimal: 2.0.1
|
||||
|
||||
is-array-buffer@3.0.5:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
|
|
@ -7365,6 +7469,8 @@ snapshots:
|
|||
call-bound: 1.0.4
|
||||
has-tostringtag: 1.0.2
|
||||
|
||||
is-decimal@2.0.1: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-finalizationregistry@1.1.1:
|
||||
|
|
@ -7383,6 +7489,8 @@ snapshots:
|
|||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-hexadecimal@2.0.1: {}
|
||||
|
||||
is-identifier@1.0.1:
|
||||
dependencies:
|
||||
identifier-regex: 1.0.1
|
||||
|
|
@ -7671,6 +7779,45 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdx-expression@2.0.1:
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.3
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdx-jsx@3.2.0:
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
'@types/unist': 3.0.3
|
||||
ccount: 2.0.1
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.3
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
parse-entities: 4.0.2
|
||||
stringify-entities: 4.0.4
|
||||
unist-util-stringify-position: 4.0.0
|
||||
vfile-message: 4.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdxjs-esm@2.0.1:
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.3
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-phrasing@4.1.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
|
|
@ -8066,6 +8213,16 @@ snapshots:
|
|||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
parse-entities@4.0.2:
|
||||
dependencies:
|
||||
'@types/unist': 2.0.11
|
||||
character-entities-legacy: 3.0.0
|
||||
character-reference-invalid: 2.0.1
|
||||
decode-named-character-reference: 1.3.0
|
||||
is-alphanumerical: 2.0.1
|
||||
is-decimal: 2.0.1
|
||||
is-hexadecimal: 2.0.1
|
||||
|
||||
parse-ms@4.0.0: {}
|
||||
|
||||
parse5@7.3.0:
|
||||
|
|
@ -8267,6 +8424,24 @@ snapshots:
|
|||
|
||||
react-is@17.0.2: {}
|
||||
|
||||
react-markdown@10.1.0(@types/react@19.2.13)(react@19.2.4):
|
||||
dependencies:
|
||||
'@types/hast': 3.0.4
|
||||
'@types/mdast': 4.0.4
|
||||
'@types/react': 19.2.13
|
||||
devlop: 1.1.0
|
||||
hast-util-to-jsx-runtime: 2.3.6
|
||||
html-url-attributes: 3.0.1
|
||||
mdast-util-to-hast: 13.2.1
|
||||
react: 19.2.4
|
||||
remark-parse: 11.0.0
|
||||
remark-rehype: 11.1.2
|
||||
unified: 11.0.5
|
||||
unist-util-visit: 5.1.0
|
||||
vfile: 6.0.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
react-redux@9.2.0(@types/react@19.2.13)(react@19.2.4)(redux@5.0.1):
|
||||
dependencies:
|
||||
'@types/use-sync-external-store': 0.0.6
|
||||
|
|
@ -8735,6 +8910,14 @@ snapshots:
|
|||
|
||||
style-mod@4.1.3: {}
|
||||
|
||||
style-to-js@1.1.21:
|
||||
dependencies:
|
||||
style-to-object: 1.0.14
|
||||
|
||||
style-to-object@1.0.14:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.7
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.29.0)(react@19.2.4):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
|
|
|
|||
|
|
@ -13,14 +13,15 @@ export async function GET(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const { searchParams, pathname } = new URL(request.url);
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
// Route to stats endpoint if requested
|
||||
if (pathname.endsWith('/stats') || searchParams.has('stats')) {
|
||||
return handleStatsRequest(request);
|
||||
return handleStatsRequest(request, workspaceId);
|
||||
}
|
||||
|
||||
// Default activities endpoint
|
||||
return handleActivitiesRequest(request);
|
||||
return handleActivitiesRequest(request, workspaceId);
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'GET /api/activities error');
|
||||
return NextResponse.json({ error: 'Failed to process request' }, { status: 500 });
|
||||
|
|
@ -30,7 +31,7 @@ export async function GET(request: NextRequest) {
|
|||
/**
|
||||
* Handle regular activities request
|
||||
*/
|
||||
async function handleActivitiesRequest(request: NextRequest) {
|
||||
async function handleActivitiesRequest(request: NextRequest, workspaceId: number) {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
|
@ -44,8 +45,8 @@ async function handleActivitiesRequest(request: NextRequest) {
|
|||
const since = searchParams.get('since'); // Unix timestamp for real-time updates
|
||||
|
||||
// Build dynamic query
|
||||
let query = 'SELECT * FROM activities WHERE 1=1';
|
||||
const params: any[] = [];
|
||||
let query = 'SELECT * FROM activities WHERE workspace_id = ?';
|
||||
const params: any[] = [workspaceId];
|
||||
|
||||
if (type) {
|
||||
query += ' AND type = ?';
|
||||
|
|
@ -74,13 +75,13 @@ async function handleActivitiesRequest(request: NextRequest) {
|
|||
const activities = stmt.all(...params) as Activity[];
|
||||
|
||||
// Prepare entity detail statements once (avoids N+1)
|
||||
const taskDetailStmt = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?');
|
||||
const agentDetailStmt = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?');
|
||||
const taskDetailStmt = db.prepare('SELECT id, title, status FROM tasks WHERE id = ? AND workspace_id = ?');
|
||||
const agentDetailStmt = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ? AND workspace_id = ?');
|
||||
const commentDetailStmt = db.prepare(`
|
||||
SELECT c.id, c.content, c.task_id, t.title as task_title
|
||||
FROM comments c
|
||||
LEFT JOIN tasks t ON c.task_id = t.id
|
||||
WHERE c.id = ?
|
||||
WHERE c.id = ? AND c.workspace_id = ? AND t.workspace_id = ?
|
||||
`);
|
||||
|
||||
// Parse JSON data field and enhance with related entity data
|
||||
|
|
@ -90,21 +91,21 @@ async function handleActivitiesRequest(request: NextRequest) {
|
|||
try {
|
||||
switch (activity.entity_type) {
|
||||
case 'task': {
|
||||
const task = taskDetailStmt.get(activity.entity_id) as any;
|
||||
const task = taskDetailStmt.get(activity.entity_id, workspaceId) as any;
|
||||
if (task) {
|
||||
entityDetails = { type: 'task', ...task };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'agent': {
|
||||
const agent = agentDetailStmt.get(activity.entity_id) as any;
|
||||
const agent = agentDetailStmt.get(activity.entity_id, workspaceId) as any;
|
||||
if (agent) {
|
||||
entityDetails = { type: 'agent', ...agent };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'comment': {
|
||||
const comment = commentDetailStmt.get(activity.entity_id) as any;
|
||||
const comment = commentDetailStmt.get(activity.entity_id, workspaceId, workspaceId) as any;
|
||||
if (comment) {
|
||||
entityDetails = {
|
||||
type: 'comment',
|
||||
|
|
@ -127,8 +128,8 @@ async function handleActivitiesRequest(request: NextRequest) {
|
|||
});
|
||||
|
||||
// Get total count for pagination
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM activities WHERE 1=1';
|
||||
const countParams: any[] = [];
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM activities WHERE workspace_id = ?';
|
||||
const countParams: any[] = [workspaceId];
|
||||
|
||||
if (type) {
|
||||
countQuery += ' AND type = ?';
|
||||
|
|
@ -166,7 +167,7 @@ async function handleActivitiesRequest(request: NextRequest) {
|
|||
/**
|
||||
* Handle stats request
|
||||
*/
|
||||
async function handleStatsRequest(request: NextRequest) {
|
||||
async function handleStatsRequest(request: NextRequest, workspaceId: number) {
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
|
@ -181,10 +182,10 @@ async function handleStatsRequest(request: NextRequest) {
|
|||
type,
|
||||
COUNT(*) as count
|
||||
FROM activities
|
||||
WHERE created_at > ?
|
||||
WHERE created_at > ? AND workspace_id = ?
|
||||
GROUP BY type
|
||||
ORDER BY count DESC
|
||||
`).all(since) as { type: string; count: number }[];
|
||||
`).all(since, workspaceId) as { type: string; count: number }[];
|
||||
|
||||
// Get most active actors
|
||||
const activeActors = db.prepare(`
|
||||
|
|
@ -192,11 +193,11 @@ async function handleStatsRequest(request: NextRequest) {
|
|||
actor,
|
||||
COUNT(*) as activity_count
|
||||
FROM activities
|
||||
WHERE created_at > ?
|
||||
WHERE created_at > ? AND workspace_id = ?
|
||||
GROUP BY actor
|
||||
ORDER BY activity_count DESC
|
||||
LIMIT 10
|
||||
`).all(since) as { actor: string; activity_count: number }[];
|
||||
`).all(since, workspaceId) as { actor: string; activity_count: number }[];
|
||||
|
||||
// Get activity timeline (hourly buckets)
|
||||
const timeline = db.prepare(`
|
||||
|
|
@ -204,10 +205,10 @@ async function handleStatsRequest(request: NextRequest) {
|
|||
(created_at / 3600) * 3600 as hour_bucket,
|
||||
COUNT(*) as count
|
||||
FROM activities
|
||||
WHERE created_at > ?
|
||||
WHERE created_at > ? AND workspace_id = ?
|
||||
GROUP BY hour_bucket
|
||||
ORDER BY hour_bucket ASC
|
||||
`).all(since) as { hour_bucket: number; count: number }[];
|
||||
`).all(since, workspaceId) as { hour_bucket: number; count: number }[];
|
||||
|
||||
return NextResponse.json({
|
||||
timeframe: `${hours} hours`,
|
||||
|
|
@ -223,4 +224,4 @@ async function handleStatsRequest(request: NextRequest) {
|
|||
logger.error({ err: error }, 'GET /api/activities (stats) error');
|
||||
return NextResponse.json({ error: 'Failed to fetch activity stats' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,15 +24,16 @@ export async function GET(
|
|||
const db = getDatabase();
|
||||
const resolvedParams = await params;
|
||||
const agentId = resolvedParams.id;
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
// Get agent by ID or name
|
||||
let agent: any;
|
||||
if (isNaN(Number(agentId))) {
|
||||
// Lookup by name
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(agentId, workspaceId);
|
||||
} else {
|
||||
// Lookup by ID
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(Number(agentId));
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(agentId), workspaceId);
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
|
|
@ -49,10 +50,12 @@ export async function GET(
|
|||
FROM comments c
|
||||
JOIN tasks t ON c.task_id = t.id
|
||||
WHERE c.mentions LIKE ?
|
||||
AND c.workspace_id = ?
|
||||
AND t.workspace_id = ?
|
||||
AND c.created_at > ?
|
||||
ORDER BY c.created_at DESC
|
||||
LIMIT 10
|
||||
`).all(`%"${agent.name}"%`, fourHoursAgo);
|
||||
`).all(`%"${agent.name}"%`, workspaceId, workspaceId, fourHoursAgo);
|
||||
|
||||
if (mentions.length > 0) {
|
||||
workItems.push({
|
||||
|
|
@ -72,10 +75,11 @@ export async function GET(
|
|||
const assignedTasks = db.prepare(`
|
||||
SELECT * FROM tasks
|
||||
WHERE assigned_to = ?
|
||||
AND workspace_id = ?
|
||||
AND status IN ('assigned', 'in_progress')
|
||||
ORDER BY priority DESC, created_at ASC
|
||||
LIMIT 10
|
||||
`).all(agent.name);
|
||||
`).all(agent.name, workspaceId);
|
||||
|
||||
if (assignedTasks.length > 0) {
|
||||
workItems.push({
|
||||
|
|
@ -92,7 +96,7 @@ export async function GET(
|
|||
}
|
||||
|
||||
// 3. Check for unread notifications
|
||||
const notifications = db_helpers.getUnreadNotifications(agent.name);
|
||||
const notifications = db_helpers.getUnreadNotifications(agent.name, workspaceId);
|
||||
|
||||
if (notifications.length > 0) {
|
||||
workItems.push({
|
||||
|
|
@ -112,11 +116,12 @@ export async function GET(
|
|||
const urgentActivities = db.prepare(`
|
||||
SELECT * FROM activities
|
||||
WHERE type IN ('task_created', 'task_assigned', 'high_priority_alert')
|
||||
AND workspace_id = ?
|
||||
AND created_at > ?
|
||||
AND description LIKE ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 5
|
||||
`).all(fourHoursAgo, `%${agent.name}%`);
|
||||
`).all(workspaceId, fourHoursAgo, `%${agent.name}%`);
|
||||
|
||||
if (urgentActivities.length > 0) {
|
||||
workItems.push({
|
||||
|
|
@ -132,7 +137,7 @@ export async function GET(
|
|||
}
|
||||
|
||||
// Update agent last_seen and status to show heartbeat activity
|
||||
db_helpers.updateAgentStatus(agent.name, 'idle', 'Heartbeat check');
|
||||
db_helpers.updateAgentStatus(agent.name, 'idle', 'Heartbeat check', workspaceId);
|
||||
|
||||
// Log heartbeat activity
|
||||
db_helpers.logActivity(
|
||||
|
|
@ -141,7 +146,8 @@ export async function GET(
|
|||
agent.id,
|
||||
agent.name,
|
||||
`Heartbeat check completed - ${workItems.length > 0 ? `${workItems.length} work items found` : 'no work items'}`,
|
||||
{ workItemsCount: workItems.length, workItemTypes: workItems.map(w => w.type) }
|
||||
{ workItemsCount: workItems.length, workItemTypes: workItems.map(w => w.type) },
|
||||
workspaceId
|
||||
);
|
||||
|
||||
if (workItems.length === 0) {
|
||||
|
|
@ -193,11 +199,12 @@ export async function POST(
|
|||
const { connection_id, token_usage } = body;
|
||||
const db = getDatabase();
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
// Update direct connection heartbeat if connection_id provided
|
||||
if (connection_id) {
|
||||
db.prepare('UPDATE direct_connections SET last_heartbeat = ?, updated_at = ? WHERE connection_id = ? AND status = ?')
|
||||
.run(now, now, connection_id, 'connected');
|
||||
db.prepare('UPDATE direct_connections SET last_heartbeat = ?, updated_at = ? WHERE connection_id = ? AND status = ? AND workspace_id = ?')
|
||||
.run(now, now, connection_id, 'connected', workspaceId);
|
||||
}
|
||||
|
||||
// Inline token reporting
|
||||
|
|
@ -207,9 +214,9 @@ export async function POST(
|
|||
const agentId = resolvedParams.id;
|
||||
let agent: any;
|
||||
if (isNaN(Number(agentId))) {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(agentId, workspaceId);
|
||||
} else {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(Number(agentId));
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(agentId), workspaceId);
|
||||
}
|
||||
|
||||
if (agent) {
|
||||
|
|
@ -230,4 +237,4 @@ export async function POST(
|
|||
...getBody,
|
||||
token_recorded: tokenRecorded,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,13 +20,14 @@ export async function GET(
|
|||
const db = getDatabase();
|
||||
const resolvedParams = await params;
|
||||
const agentId = resolvedParams.id;
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
// Get agent by ID or name
|
||||
let agent: any;
|
||||
if (isNaN(Number(agentId))) {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(agentId, workspaceId);
|
||||
} else {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(Number(agentId));
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(agentId), workspaceId);
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
|
|
@ -43,8 +44,8 @@ export async function GET(
|
|||
}
|
||||
|
||||
// Get working memory content
|
||||
const memoryStmt = db.prepare(`SELECT working_memory FROM agents WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ?`);
|
||||
const result = memoryStmt.get(agentId) as any;
|
||||
const memoryStmt = db.prepare(`SELECT working_memory FROM agents WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ? AND workspace_id = ?`);
|
||||
const result = memoryStmt.get(agentId, workspaceId) as any;
|
||||
|
||||
const workingMemory = result?.working_memory || '';
|
||||
|
||||
|
|
@ -78,15 +79,16 @@ export async function PUT(
|
|||
const db = getDatabase();
|
||||
const resolvedParams = await params;
|
||||
const agentId = resolvedParams.id;
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const body = await request.json();
|
||||
const { working_memory, append } = body;
|
||||
|
||||
// Get agent by ID or name
|
||||
let agent: any;
|
||||
if (isNaN(Number(agentId))) {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(agentId, workspaceId);
|
||||
} else {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(Number(agentId));
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(agentId), workspaceId);
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
|
|
@ -105,8 +107,8 @@ export async function PUT(
|
|||
|
||||
// Handle append mode
|
||||
if (append) {
|
||||
const currentStmt = db.prepare(`SELECT working_memory FROM agents WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ?`);
|
||||
const current = currentStmt.get(agentId) as any;
|
||||
const currentStmt = db.prepare(`SELECT working_memory FROM agents WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ? AND workspace_id = ?`);
|
||||
const current = currentStmt.get(agentId, workspaceId) as any;
|
||||
const currentContent = current?.working_memory || '';
|
||||
|
||||
// Add timestamp and append
|
||||
|
|
@ -121,10 +123,10 @@ export async function PUT(
|
|||
const updateStmt = db.prepare(`
|
||||
UPDATE agents
|
||||
SET working_memory = ?, updated_at = ?
|
||||
WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ?
|
||||
WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ? AND workspace_id = ?
|
||||
`);
|
||||
|
||||
updateStmt.run(newContent, now, agentId);
|
||||
updateStmt.run(newContent, now, agentId, workspaceId);
|
||||
|
||||
// Log activity
|
||||
db_helpers.logActivity(
|
||||
|
|
@ -137,7 +139,8 @@ export async function PUT(
|
|||
content_length: newContent.length,
|
||||
append_mode: append || false,
|
||||
timestamp: now
|
||||
}
|
||||
},
|
||||
workspaceId
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
|
|
@ -167,13 +170,14 @@ export async function DELETE(
|
|||
const db = getDatabase();
|
||||
const resolvedParams = await params;
|
||||
const agentId = resolvedParams.id;
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
// Get agent by ID or name
|
||||
let agent: any;
|
||||
if (isNaN(Number(agentId))) {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(agentId, workspaceId);
|
||||
} else {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(Number(agentId));
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(agentId), workspaceId);
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
|
|
@ -186,10 +190,10 @@ export async function DELETE(
|
|||
const updateStmt = db.prepare(`
|
||||
UPDATE agents
|
||||
SET working_memory = '', updated_at = ?
|
||||
WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ?
|
||||
WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ? AND workspace_id = ?
|
||||
`);
|
||||
|
||||
updateStmt.run(now, agentId);
|
||||
updateStmt.run(now, agentId, workspaceId);
|
||||
|
||||
// Log activity
|
||||
db_helpers.logActivity(
|
||||
|
|
@ -198,7 +202,8 @@ export async function DELETE(
|
|||
agent.id,
|
||||
agent.name,
|
||||
`Working memory cleared for agent ${agent.name}`,
|
||||
{ timestamp: now }
|
||||
{ timestamp: now },
|
||||
workspaceId
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getDatabase, db_helpers, logAuditEvent } from '@/lib/db'
|
||||
import { getUserFromRequest, requireRole } from '@/lib/auth'
|
||||
import { requireRole } from '@/lib/auth'
|
||||
import { writeAgentToConfig, enrichAgentConfigFromWorkspace } from '@/lib/agent-sync'
|
||||
import { eventBus } from '@/lib/event-bus'
|
||||
import { logger } from '@/lib/logger'
|
||||
|
|
@ -18,12 +18,13 @@ export async function GET(
|
|||
try {
|
||||
const db = getDatabase()
|
||||
const { id } = await params
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
let agent
|
||||
if (isNaN(Number(id))) {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(id)
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(id, workspaceId)
|
||||
} else {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(Number(id))
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(id), workspaceId)
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
|
|
@ -61,14 +62,15 @@ export async function PUT(
|
|||
try {
|
||||
const db = getDatabase()
|
||||
const { id } = await params
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const body = await request.json()
|
||||
const { role, gateway_config, write_to_gateway } = body
|
||||
|
||||
let agent
|
||||
if (isNaN(Number(id))) {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(id) as any
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(id, workspaceId) as any
|
||||
} else {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(Number(id)) as any
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(id), workspaceId) as any
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
|
|
@ -98,8 +100,8 @@ export async function PUT(
|
|||
values.push(JSON.stringify(newConfig))
|
||||
}
|
||||
|
||||
values.push(agent.id)
|
||||
db.prepare(`UPDATE agents SET ${fields.join(', ')} WHERE id = ?`).run(...values)
|
||||
values.push(agent.id, workspaceId)
|
||||
db.prepare(`UPDATE agents SET ${fields.join(', ')} WHERE id = ? AND workspace_id = ?`).run(...values)
|
||||
|
||||
// Write back to openclaw.json if requested
|
||||
if (write_to_gateway && gateway_config) {
|
||||
|
|
@ -143,7 +145,8 @@ export async function PUT(
|
|||
agent.id,
|
||||
auth.user.username,
|
||||
`Config updated for agent ${agent.name}${write_to_gateway ? ' (+ gateway)' : ''}`,
|
||||
{ fields: Object.keys(gateway_config || {}), write_to_gateway }
|
||||
{ fields: Object.keys(gateway_config || {}), write_to_gateway },
|
||||
workspaceId
|
||||
)
|
||||
|
||||
// Broadcast update
|
||||
|
|
@ -179,19 +182,20 @@ export async function DELETE(
|
|||
try {
|
||||
const db = getDatabase()
|
||||
const { id } = await params
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
let agent
|
||||
if (isNaN(Number(id))) {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(id) as any
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(id, workspaceId) as any
|
||||
} else {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(Number(id)) as any
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(id), workspaceId) as any
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
db.prepare('DELETE FROM agents WHERE id = ?').run(agent.id)
|
||||
db.prepare('DELETE FROM agents WHERE id = ? AND workspace_id = ?').run(agent.id, workspaceId)
|
||||
|
||||
db_helpers.logActivity(
|
||||
'agent_deleted',
|
||||
|
|
@ -199,7 +203,8 @@ export async function DELETE(
|
|||
agent.id,
|
||||
auth.user.username,
|
||||
`Deleted agent: ${agent.name}`,
|
||||
{ name: agent.name, role: agent.role }
|
||||
{ name: agent.name, role: agent.role },
|
||||
workspaceId
|
||||
)
|
||||
|
||||
eventBus.broadcast('agent.deleted', { id: agent.id, name: agent.name })
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from
|
|||
import { join, dirname } from 'path';
|
||||
import { config } from '@/lib/config';
|
||||
import { resolveWithin } from '@/lib/paths';
|
||||
import { getUserFromRequest, requireRole } from '@/lib/auth';
|
||||
import { requireRole } from '@/lib/auth';
|
||||
import { logger } from '@/lib/logger';
|
||||
|
||||
/**
|
||||
|
|
@ -21,13 +21,14 @@ export async function GET(
|
|||
const db = getDatabase();
|
||||
const resolvedParams = await params;
|
||||
const agentId = resolvedParams.id;
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
// Get agent by ID or name
|
||||
let agent: any;
|
||||
if (isNaN(Number(agentId))) {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(agentId, workspaceId);
|
||||
} else {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(Number(agentId));
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(agentId), workspaceId);
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
|
|
@ -103,15 +104,16 @@ export async function PUT(
|
|||
const db = getDatabase();
|
||||
const resolvedParams = await params;
|
||||
const agentId = resolvedParams.id;
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const body = await request.json();
|
||||
const { soul_content, template_name } = body;
|
||||
|
||||
// Get agent by ID or name
|
||||
let agent: any;
|
||||
if (isNaN(Number(agentId))) {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId);
|
||||
agent = db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(agentId, workspaceId);
|
||||
} else {
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ?').get(Number(agentId));
|
||||
agent = db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(agentId), workspaceId);
|
||||
}
|
||||
|
||||
if (!agent) {
|
||||
|
|
@ -170,24 +172,25 @@ export async function PUT(
|
|||
const updateStmt = db.prepare(`
|
||||
UPDATE agents
|
||||
SET soul_content = ?, updated_at = ?
|
||||
WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ?
|
||||
WHERE ${isNaN(Number(agentId)) ? 'name' : 'id'} = ? AND workspace_id = ?
|
||||
`);
|
||||
|
||||
updateStmt.run(newSoulContent, now, agentId);
|
||||
updateStmt.run(newSoulContent, now, agentId, workspaceId);
|
||||
|
||||
// Log activity
|
||||
db_helpers.logActivity(
|
||||
'agent_soul_updated',
|
||||
'agent',
|
||||
agent.id,
|
||||
getUserFromRequest(request)?.username || 'system',
|
||||
auth.user.username,
|
||||
`SOUL content updated for agent ${agent.name}${template_name ? ` using template: ${template_name}` : ''}${savedToWorkspace ? ' (synced to workspace)' : ''}`,
|
||||
{
|
||||
template_used: template_name || null,
|
||||
content_length: newSoulContent ? newSoulContent.length : 0,
|
||||
previous_content_length: agent.soul_content ? agent.soul_content.length : 0,
|
||||
saved_to_workspace: savedToWorkspace
|
||||
}
|
||||
},
|
||||
workspaceId
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
|
|
|
|||
|
|
@ -14,14 +14,15 @@ export async function POST(
|
|||
try {
|
||||
const resolvedParams = await params
|
||||
const agentId = resolvedParams.id
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const body = await request.json().catch(() => ({}))
|
||||
const customMessage =
|
||||
typeof body?.message === 'string' ? body.message.trim() : ''
|
||||
|
||||
const db = getDatabase()
|
||||
const agent: any = isNaN(Number(agentId))
|
||||
? db.prepare('SELECT * FROM agents WHERE name = ?').get(agentId)
|
||||
: db.prepare('SELECT * FROM agents WHERE id = ?').get(Number(agentId))
|
||||
? db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(agentId, workspaceId)
|
||||
: db.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?').get(Number(agentId), workspaceId)
|
||||
|
||||
if (!agent) {
|
||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
|
||||
|
|
@ -50,7 +51,7 @@ export async function POST(
|
|||
)
|
||||
}
|
||||
|
||||
db_helpers.updateAgentStatus(agent.name, 'idle', 'Manual wake')
|
||||
db_helpers.updateAgentStatus(agent.name, 'idle', 'Manual wake', workspaceId)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const db = getDatabase()
|
||||
const { searchParams } = new URL(request.url)
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
|
||||
const limit = parseInt(searchParams.get("limit") || "100")
|
||||
const offset = parseInt(searchParams.get("offset") || "0")
|
||||
|
|
@ -27,11 +28,12 @@ export async function GET(request: NextRequest) {
|
|||
// 1. Get inter-agent messages
|
||||
let messagesQuery = `
|
||||
SELECT * FROM messages
|
||||
WHERE to_agent IS NOT NULL
|
||||
WHERE workspace_id = ?
|
||||
AND to_agent IS NOT NULL
|
||||
AND from_agent NOT IN (${humanPlaceholders})
|
||||
AND to_agent NOT IN (${humanPlaceholders})
|
||||
`
|
||||
const messagesParams: any[] = [...humanNames, ...humanNames]
|
||||
const messagesParams: any[] = [workspaceId, ...humanNames, ...humanNames]
|
||||
|
||||
if (since) {
|
||||
messagesQuery += " AND created_at > ?"
|
||||
|
|
@ -55,11 +57,12 @@ export async function GET(request: NextRequest) {
|
|||
COUNT(*) as message_count,
|
||||
MAX(created_at) as last_message_at
|
||||
FROM messages
|
||||
WHERE to_agent IS NOT NULL
|
||||
WHERE workspace_id = ?
|
||||
AND to_agent IS NOT NULL
|
||||
AND from_agent NOT IN (${humanPlaceholders})
|
||||
AND to_agent NOT IN (${humanPlaceholders})
|
||||
`
|
||||
const graphParams: any[] = [...humanNames, ...humanNames]
|
||||
const graphParams: any[] = [workspaceId, ...humanNames, ...humanNames]
|
||||
if (since) {
|
||||
graphQuery += " AND created_at > ?"
|
||||
graphParams.push(parseInt(since))
|
||||
|
|
@ -72,29 +75,30 @@ export async function GET(request: NextRequest) {
|
|||
const statsQuery = `
|
||||
SELECT agent, SUM(sent) as sent, SUM(received) as received FROM (
|
||||
SELECT from_agent as agent, COUNT(*) as sent, 0 as received
|
||||
FROM messages WHERE to_agent IS NOT NULL
|
||||
FROM messages WHERE workspace_id = ? AND to_agent IS NOT NULL
|
||||
AND from_agent NOT IN (${humanPlaceholders})
|
||||
AND to_agent NOT IN (${humanPlaceholders})
|
||||
GROUP BY from_agent
|
||||
UNION ALL
|
||||
SELECT to_agent as agent, 0 as sent, COUNT(*) as received
|
||||
FROM messages WHERE to_agent IS NOT NULL
|
||||
FROM messages WHERE workspace_id = ? AND to_agent IS NOT NULL
|
||||
AND from_agent NOT IN (${humanPlaceholders})
|
||||
AND to_agent NOT IN (${humanPlaceholders})
|
||||
GROUP BY to_agent
|
||||
) GROUP BY agent ORDER BY (sent + received) DESC
|
||||
`
|
||||
const statsParams = [...humanNames, ...humanNames, ...humanNames, ...humanNames]
|
||||
const statsParams = [workspaceId, ...humanNames, ...humanNames, workspaceId, ...humanNames, ...humanNames]
|
||||
const agentStats = db.prepare(statsQuery).all(...statsParams)
|
||||
|
||||
// 4. Total count
|
||||
let countQuery = `
|
||||
SELECT COUNT(*) as total FROM messages
|
||||
WHERE to_agent IS NOT NULL
|
||||
WHERE workspace_id = ?
|
||||
AND to_agent IS NOT NULL
|
||||
AND from_agent NOT IN (${humanPlaceholders})
|
||||
AND to_agent NOT IN (${humanPlaceholders})
|
||||
`
|
||||
const countParams: any[] = [...humanNames, ...humanNames]
|
||||
const countParams: any[] = [workspaceId, ...humanNames, ...humanNames]
|
||||
if (since) {
|
||||
countQuery += " AND created_at > ?"
|
||||
countParams.push(parseInt(since))
|
||||
|
|
@ -107,12 +111,13 @@ export async function GET(request: NextRequest) {
|
|||
|
||||
let seededCountQuery = `
|
||||
SELECT COUNT(*) as seeded FROM messages
|
||||
WHERE to_agent IS NOT NULL
|
||||
WHERE workspace_id = ?
|
||||
AND to_agent IS NOT NULL
|
||||
AND from_agent NOT IN (${humanPlaceholders})
|
||||
AND to_agent NOT IN (${humanPlaceholders})
|
||||
AND conversation_id LIKE ?
|
||||
`
|
||||
const seededParams: any[] = [...humanNames, ...humanNames, "conv-multi-%"]
|
||||
const seededParams: any[] = [workspaceId, ...humanNames, ...humanNames, "conv-multi-%"]
|
||||
if (since) {
|
||||
seededCountQuery += " AND created_at > ?"
|
||||
seededParams.push(parseInt(since))
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ export async function POST(request: NextRequest) {
|
|||
const { from, to, message } = result.data
|
||||
|
||||
const db = getDatabase()
|
||||
const agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(to) as any
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const agent = db
|
||||
.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?')
|
||||
.get(to, workspaceId) as any
|
||||
if (!agent) {
|
||||
return NextResponse.json({ error: 'Recipient agent not found' }, { status: 404 })
|
||||
}
|
||||
|
|
@ -48,7 +51,8 @@ export async function POST(request: NextRequest) {
|
|||
'Direct Message',
|
||||
`${from}: ${message.substring(0, 200)}${message.length > 200 ? '...' : ''}`,
|
||||
'agent',
|
||||
agent.id
|
||||
agent.id,
|
||||
workspaceId
|
||||
)
|
||||
|
||||
db_helpers.logActivity(
|
||||
|
|
@ -57,7 +61,8 @@ export async function POST(request: NextRequest) {
|
|||
agent.id,
|
||||
from,
|
||||
`Sent message to ${to}`,
|
||||
{ to }
|
||||
{ to },
|
||||
workspaceId
|
||||
)
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { eventBus } from '@/lib/event-bus';
|
|||
import { getTemplate, buildAgentConfig } from '@/lib/agent-templates';
|
||||
import { writeAgentToConfig, enrichAgentConfigFromWorkspace } from '@/lib/agent-sync';
|
||||
import { logAuditEvent } from '@/lib/db';
|
||||
import { getUserFromRequest, requireRole } from '@/lib/auth';
|
||||
import { requireRole } from '@/lib/auth';
|
||||
import { mutationLimiter } from '@/lib/rate-limit';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { validateBody, createAgentSchema } from '@/lib/validation';
|
||||
|
|
@ -20,6 +20,7 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const db = getDatabase();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
// Parse query parameters
|
||||
const status = searchParams.get('status');
|
||||
|
|
@ -28,8 +29,8 @@ export async function GET(request: NextRequest) {
|
|||
const offset = parseInt(searchParams.get('offset') || '0');
|
||||
|
||||
// Build dynamic query
|
||||
let query = 'SELECT * FROM agents WHERE 1=1';
|
||||
const params: any[] = [];
|
||||
let query = 'SELECT * FROM agents WHERE workspace_id = ?';
|
||||
const params: any[] = [workspaceId];
|
||||
|
||||
if (status) {
|
||||
query += ' AND status = ?';
|
||||
|
|
@ -61,11 +62,11 @@ export async function GET(request: NextRequest) {
|
|||
SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) as in_progress,
|
||||
SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) as completed
|
||||
FROM tasks
|
||||
WHERE assigned_to = ?
|
||||
WHERE assigned_to = ? AND workspace_id = ?
|
||||
`);
|
||||
|
||||
const agentsWithStats = agentsWithParsedData.map(agent => {
|
||||
const taskStats = taskCountStmt.get(agent.name) as any;
|
||||
const taskStats = taskCountStmt.get(agent.name, workspaceId) as any;
|
||||
|
||||
return {
|
||||
...agent,
|
||||
|
|
@ -79,8 +80,8 @@ export async function GET(request: NextRequest) {
|
|||
});
|
||||
|
||||
// Get total count for pagination
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM agents WHERE 1=1';
|
||||
const countParams: any[] = [];
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM agents WHERE workspace_id = ?';
|
||||
const countParams: any[] = [workspaceId];
|
||||
if (status) {
|
||||
countQuery += ' AND status = ?';
|
||||
countParams.push(status);
|
||||
|
|
@ -115,6 +116,7 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const validated = await validateBody(request, createAgentSchema);
|
||||
if ('error' in validated) return validated.error;
|
||||
const body = validated.data;
|
||||
|
|
@ -150,7 +152,9 @@ export async function POST(request: NextRequest) {
|
|||
}
|
||||
|
||||
// Check if agent name already exists
|
||||
const existingAgent = db.prepare('SELECT id FROM agents WHERE name = ?').get(name);
|
||||
const existingAgent = db
|
||||
.prepare('SELECT id FROM agents WHERE name = ? AND workspace_id = ?')
|
||||
.get(name, workspaceId);
|
||||
if (existingAgent) {
|
||||
return NextResponse.json({ error: 'Agent name already exists' }, { status: 409 });
|
||||
}
|
||||
|
|
@ -160,8 +164,8 @@ export async function POST(request: NextRequest) {
|
|||
const stmt = db.prepare(`
|
||||
INSERT INTO agents (
|
||||
name, role, session_key, soul_content, status,
|
||||
created_at, updated_at, config
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
created_at, updated_at, config, workspace_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const dbResult = stmt.run(
|
||||
|
|
@ -172,7 +176,8 @@ export async function POST(request: NextRequest) {
|
|||
status,
|
||||
now,
|
||||
now,
|
||||
JSON.stringify(finalConfig)
|
||||
JSON.stringify(finalConfig),
|
||||
workspaceId
|
||||
);
|
||||
|
||||
const agentId = dbResult.lastInsertRowid as number;
|
||||
|
|
@ -182,7 +187,7 @@ export async function POST(request: NextRequest) {
|
|||
'agent_created',
|
||||
'agent',
|
||||
agentId,
|
||||
getUserFromRequest(request)?.username || 'system',
|
||||
auth.user.username,
|
||||
`Created agent: ${name} (${finalRole})${template ? ` from template: ${template}` : ''}`,
|
||||
{
|
||||
name,
|
||||
|
|
@ -190,11 +195,14 @@ export async function POST(request: NextRequest) {
|
|||
status,
|
||||
session_key,
|
||||
template: template || null
|
||||
}
|
||||
},
|
||||
workspaceId
|
||||
);
|
||||
|
||||
// Fetch the created agent
|
||||
const createdAgent = db.prepare('SELECT * FROM agents WHERE id = ?').get(agentId) as Agent;
|
||||
const createdAgent = db
|
||||
.prepare('SELECT * FROM agents WHERE id = ? AND workspace_id = ?')
|
||||
.get(agentId, workspaceId) as Agent;
|
||||
const parsedAgent = {
|
||||
...createdAgent,
|
||||
config: JSON.parse(createdAgent.config || '{}'),
|
||||
|
|
@ -222,7 +230,8 @@ export async function POST(request: NextRequest) {
|
|||
const ipAddress = request.headers.get('x-forwarded-for') || 'unknown';
|
||||
logAuditEvent({
|
||||
action: 'agent_gateway_create',
|
||||
actor: getUserFromRequest(request)?.username || 'system',
|
||||
actor: auth.user.username,
|
||||
actor_id: auth.user.id,
|
||||
target_type: 'agent',
|
||||
target_id: agentId as number,
|
||||
detail: { name, openclaw_id: openclawId, template: template || null },
|
||||
|
|
@ -256,6 +265,7 @@ export async function PUT(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const body = await request.json();
|
||||
|
||||
// Handle single agent update or bulk updates
|
||||
|
|
@ -263,7 +273,9 @@ export async function PUT(request: NextRequest) {
|
|||
// Single agent update
|
||||
const { name, status, last_activity, config, session_key, soul_content, role } = body;
|
||||
|
||||
const agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(name) as Agent;
|
||||
const agent = db
|
||||
.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?')
|
||||
.get(name, workspaceId) as Agent;
|
||||
if (!agent) {
|
||||
return NextResponse.json({ error: 'Agent not found' }, { status: 404 });
|
||||
}
|
||||
|
|
@ -309,7 +321,7 @@ export async function PUT(request: NextRequest) {
|
|||
|
||||
fieldsToUpdate.push('updated_at = ?');
|
||||
params.push(now);
|
||||
params.push(name);
|
||||
params.push(name, workspaceId);
|
||||
|
||||
if (fieldsToUpdate.length === 1) { // Only updated_at
|
||||
return NextResponse.json({ error: 'No fields to update' }, { status: 400 });
|
||||
|
|
@ -318,7 +330,7 @@ export async function PUT(request: NextRequest) {
|
|||
const stmt = db.prepare(`
|
||||
UPDATE agents
|
||||
SET ${fieldsToUpdate.join(', ')}
|
||||
WHERE name = ?
|
||||
WHERE name = ? AND workspace_id = ?
|
||||
`);
|
||||
|
||||
stmt.run(...params);
|
||||
|
|
@ -335,7 +347,8 @@ export async function PUT(request: NextRequest) {
|
|||
oldStatus: agent.status,
|
||||
newStatus: status,
|
||||
last_activity
|
||||
}
|
||||
},
|
||||
workspaceId
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,11 @@ export async function GET(request: NextRequest) {
|
|||
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
try {
|
||||
const rules = db.prepare('SELECT * FROM alert_rules ORDER BY created_at DESC').all() as AlertRule[]
|
||||
const rules = db
|
||||
.prepare('SELECT * FROM alert_rules WHERE workspace_id = ? ORDER BY created_at DESC')
|
||||
.all(workspaceId) as AlertRule[]
|
||||
return NextResponse.json({ rules })
|
||||
} catch {
|
||||
return NextResponse.json({ rules: [] })
|
||||
|
|
@ -50,6 +53,7 @@ export async function POST(request: NextRequest) {
|
|||
if (rateCheck) return rateCheck
|
||||
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
|
||||
// Check for evaluate action first (peek at body without consuming)
|
||||
let rawBody: any
|
||||
|
|
@ -58,7 +62,7 @@ export async function POST(request: NextRequest) {
|
|||
}
|
||||
|
||||
if (rawBody.action === 'evaluate') {
|
||||
return evaluateRules(db)
|
||||
return evaluateRules(db, workspaceId)
|
||||
}
|
||||
|
||||
// Validate for create using schema
|
||||
|
|
@ -73,8 +77,8 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const result = db.prepare(`
|
||||
INSERT INTO alert_rules (name, description, entity_type, condition_field, condition_operator, condition_value, action_type, action_config, cooldown_minutes, created_by)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO alert_rules (name, description, entity_type, condition_field, condition_operator, condition_value, action_type, action_config, cooldown_minutes, created_by, workspace_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
name,
|
||||
description || null,
|
||||
|
|
@ -85,7 +89,8 @@ export async function POST(request: NextRequest) {
|
|||
action_type || 'notification',
|
||||
JSON.stringify(action_config || {}),
|
||||
cooldown_minutes || 60,
|
||||
auth.user?.username || 'system'
|
||||
auth.user?.username || 'system',
|
||||
workspaceId
|
||||
)
|
||||
|
||||
// Audit log
|
||||
|
|
@ -97,7 +102,9 @@ export async function POST(request: NextRequest) {
|
|||
)
|
||||
} catch { /* audit table might not exist */ }
|
||||
|
||||
const rule = db.prepare('SELECT * FROM alert_rules WHERE id = ?').get(result.lastInsertRowid) as AlertRule
|
||||
const rule = db
|
||||
.prepare('SELECT * FROM alert_rules WHERE id = ? AND workspace_id = ?')
|
||||
.get(result.lastInsertRowid, workspaceId) as AlertRule
|
||||
return NextResponse.json({ rule }, { status: 201 })
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message || 'Failed to create rule' }, { status: 500 })
|
||||
|
|
@ -115,12 +122,15 @@ export async function PUT(request: NextRequest) {
|
|||
if (rateCheck) return rateCheck
|
||||
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
const body = await request.json()
|
||||
const { id, ...updates } = body
|
||||
|
||||
if (!id) return NextResponse.json({ error: 'id is required' }, { status: 400 })
|
||||
|
||||
const existing = db.prepare('SELECT * FROM alert_rules WHERE id = ?').get(id) as AlertRule | undefined
|
||||
const existing = db
|
||||
.prepare('SELECT * FROM alert_rules WHERE id = ? AND workspace_id = ?')
|
||||
.get(id, workspaceId) as AlertRule | undefined
|
||||
if (!existing) return NextResponse.json({ error: 'Rule not found' }, { status: 404 })
|
||||
|
||||
const allowed = ['name', 'description', 'enabled', 'entity_type', 'condition_field', 'condition_operator', 'condition_value', 'action_type', 'action_config', 'cooldown_minutes']
|
||||
|
|
@ -137,11 +147,13 @@ export async function PUT(request: NextRequest) {
|
|||
if (sets.length === 0) return NextResponse.json({ error: 'No valid fields to update' }, { status: 400 })
|
||||
|
||||
sets.push('updated_at = (unixepoch())')
|
||||
values.push(id)
|
||||
values.push(id, workspaceId)
|
||||
|
||||
db.prepare(`UPDATE alert_rules SET ${sets.join(', ')} WHERE id = ?`).run(...values)
|
||||
db.prepare(`UPDATE alert_rules SET ${sets.join(', ')} WHERE id = ? AND workspace_id = ?`).run(...values)
|
||||
|
||||
const updated = db.prepare('SELECT * FROM alert_rules WHERE id = ?').get(id) as AlertRule
|
||||
const updated = db
|
||||
.prepare('SELECT * FROM alert_rules WHERE id = ? AND workspace_id = ?')
|
||||
.get(id, workspaceId) as AlertRule
|
||||
return NextResponse.json({ rule: updated })
|
||||
}
|
||||
|
||||
|
|
@ -156,12 +168,13 @@ export async function DELETE(request: NextRequest) {
|
|||
if (rateCheck) return rateCheck
|
||||
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
const body = await request.json()
|
||||
const { id } = body
|
||||
|
||||
if (!id) return NextResponse.json({ error: 'id is required' }, { status: 400 })
|
||||
|
||||
const result = db.prepare('DELETE FROM alert_rules WHERE id = ?').run(id)
|
||||
const result = db.prepare('DELETE FROM alert_rules WHERE id = ? AND workspace_id = ?').run(id, workspaceId)
|
||||
|
||||
try {
|
||||
db.prepare('INSERT INTO audit_log (action, actor, detail) VALUES (?, ?, ?)').run(
|
||||
|
|
@ -177,10 +190,10 @@ export async function DELETE(request: NextRequest) {
|
|||
/**
|
||||
* Evaluate all enabled alert rules against current data
|
||||
*/
|
||||
function evaluateRules(db: ReturnType<typeof getDatabase>) {
|
||||
function evaluateRules(db: ReturnType<typeof getDatabase>, workspaceId: number) {
|
||||
let rules: AlertRule[]
|
||||
try {
|
||||
rules = db.prepare('SELECT * FROM alert_rules WHERE enabled = 1').all() as AlertRule[]
|
||||
rules = db.prepare('SELECT * FROM alert_rules WHERE enabled = 1 AND workspace_id = ?').all(workspaceId) as AlertRule[]
|
||||
} catch {
|
||||
return NextResponse.json({ evaluated: 0, triggered: 0, results: [] })
|
||||
}
|
||||
|
|
@ -195,7 +208,7 @@ function evaluateRules(db: ReturnType<typeof getDatabase>) {
|
|||
continue
|
||||
}
|
||||
|
||||
const triggered = evaluateRule(db, rule, now)
|
||||
const triggered = evaluateRule(db, rule, now, workspaceId)
|
||||
results.push({ rule_id: rule.id, rule_name: rule.name, triggered, reason: triggered ? 'Condition met' : 'Condition not met' })
|
||||
|
||||
if (triggered) {
|
||||
|
|
@ -207,9 +220,9 @@ function evaluateRules(db: ReturnType<typeof getDatabase>) {
|
|||
const config = JSON.parse(rule.action_config || '{}')
|
||||
const recipient = config.recipient || 'system'
|
||||
db.prepare(`
|
||||
INSERT INTO notifications (recipient, type, title, message, source_type, source_id)
|
||||
VALUES (?, 'alert', ?, ?, 'alert_rule', ?)
|
||||
`).run(recipient, `Alert: ${rule.name}`, rule.description || `Rule "${rule.name}" triggered`, rule.id)
|
||||
INSERT INTO notifications (recipient, type, title, message, source_type, source_id, workspace_id)
|
||||
VALUES (?, 'alert', ?, ?, 'alert_rule', ?, ?)
|
||||
`).run(recipient, `Alert: ${rule.name}`, rule.description || `Rule "${rule.name}" triggered`, rule.id, workspaceId)
|
||||
} catch { /* notification creation failed */ }
|
||||
}
|
||||
}
|
||||
|
|
@ -218,13 +231,13 @@ function evaluateRules(db: ReturnType<typeof getDatabase>) {
|
|||
return NextResponse.json({ evaluated: rules.length, triggered, results })
|
||||
}
|
||||
|
||||
function evaluateRule(db: ReturnType<typeof getDatabase>, rule: AlertRule, now: number): boolean {
|
||||
function evaluateRule(db: ReturnType<typeof getDatabase>, rule: AlertRule, now: number, workspaceId: number): boolean {
|
||||
try {
|
||||
switch (rule.entity_type) {
|
||||
case 'agent': return evaluateAgentRule(db, rule, now)
|
||||
case 'task': return evaluateTaskRule(db, rule, now)
|
||||
case 'session': return evaluateSessionRule(db, rule, now)
|
||||
case 'activity': return evaluateActivityRule(db, rule, now)
|
||||
case 'agent': return evaluateAgentRule(db, rule, now, workspaceId)
|
||||
case 'task': return evaluateTaskRule(db, rule, now, workspaceId)
|
||||
case 'session': return evaluateSessionRule(db, rule, now, workspaceId)
|
||||
case 'activity': return evaluateActivityRule(db, rule, now, workspaceId)
|
||||
default: return false
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -232,61 +245,61 @@ function evaluateRule(db: ReturnType<typeof getDatabase>, rule: AlertRule, now:
|
|||
}
|
||||
}
|
||||
|
||||
function evaluateAgentRule(db: ReturnType<typeof getDatabase>, rule: AlertRule, now: number): boolean {
|
||||
function evaluateAgentRule(db: ReturnType<typeof getDatabase>, rule: AlertRule, now: number, workspaceId: number): boolean {
|
||||
const { condition_field, condition_operator, condition_value } = rule
|
||||
|
||||
if (condition_operator === 'count_above' || condition_operator === 'count_below') {
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM agents WHERE ${safeColumn('agents', condition_field)} = ?`).get(condition_value) as any)?.c || 0
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM agents WHERE workspace_id = ? AND ${safeColumn('agents', condition_field)} = ?`).get(workspaceId, condition_value) as any)?.c || 0
|
||||
return condition_operator === 'count_above' ? count > parseInt(condition_value) : count < parseInt(condition_value)
|
||||
}
|
||||
|
||||
if (condition_operator === 'age_minutes_above') {
|
||||
// Check agents where field value is older than N minutes (e.g., last_seen)
|
||||
const threshold = now - parseInt(condition_value) * 60
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM agents WHERE status != 'offline' AND ${safeColumn('agents', condition_field)} < ?`).get(threshold) as any)?.c || 0
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM agents WHERE workspace_id = ? AND status != 'offline' AND ${safeColumn('agents', condition_field)} < ?`).get(workspaceId, threshold) as any)?.c || 0
|
||||
return count > 0
|
||||
}
|
||||
|
||||
const agents = db.prepare(`SELECT ${safeColumn('agents', condition_field)} as val FROM agents WHERE status != 'offline'`).all() as any[]
|
||||
const agents = db.prepare(`SELECT ${safeColumn('agents', condition_field)} as val FROM agents WHERE workspace_id = ? AND status != 'offline'`).all(workspaceId) as any[]
|
||||
return agents.some(a => compareValue(a.val, condition_operator, condition_value))
|
||||
}
|
||||
|
||||
function evaluateTaskRule(db: ReturnType<typeof getDatabase>, rule: AlertRule, _now: number): boolean {
|
||||
function evaluateTaskRule(db: ReturnType<typeof getDatabase>, rule: AlertRule, _now: number, workspaceId: number): boolean {
|
||||
const { condition_field, condition_operator, condition_value } = rule
|
||||
|
||||
if (condition_operator === 'count_above') {
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM tasks WHERE ${safeColumn('tasks', condition_field)} = ?`).get(condition_value) as any)?.c || 0
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM tasks WHERE workspace_id = ? AND ${safeColumn('tasks', condition_field)} = ?`).get(workspaceId, condition_value) as any)?.c || 0
|
||||
return count > parseInt(condition_value)
|
||||
}
|
||||
|
||||
if (condition_operator === 'count_below') {
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM tasks`).get() as any)?.c || 0
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM tasks WHERE workspace_id = ?`).get(workspaceId) as any)?.c || 0
|
||||
return count < parseInt(condition_value)
|
||||
}
|
||||
|
||||
const tasks = db.prepare(`SELECT ${safeColumn('tasks', condition_field)} as val FROM tasks`).all() as any[]
|
||||
const tasks = db.prepare(`SELECT ${safeColumn('tasks', condition_field)} as val FROM tasks WHERE workspace_id = ?`).all(workspaceId) as any[]
|
||||
return tasks.some(t => compareValue(t.val, condition_operator, condition_value))
|
||||
}
|
||||
|
||||
function evaluateSessionRule(db: ReturnType<typeof getDatabase>, rule: AlertRule, _now: number): boolean {
|
||||
function evaluateSessionRule(db: ReturnType<typeof getDatabase>, rule: AlertRule, _now: number, workspaceId: number): boolean {
|
||||
// Session data comes from the gateway, not the DB, so we check the agents table for session info
|
||||
const { condition_operator, condition_value } = rule
|
||||
|
||||
if (condition_operator === 'count_above') {
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM agents WHERE status = 'busy'`).get() as any)?.c || 0
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM agents WHERE workspace_id = ? AND status = 'busy'`).get(workspaceId) as any)?.c || 0
|
||||
return count > parseInt(condition_value)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function evaluateActivityRule(db: ReturnType<typeof getDatabase>, rule: AlertRule, now: number): boolean {
|
||||
function evaluateActivityRule(db: ReturnType<typeof getDatabase>, rule: AlertRule, now: number, workspaceId: number): boolean {
|
||||
const { condition_field, condition_operator, condition_value } = rule
|
||||
|
||||
if (condition_operator === 'count_above') {
|
||||
// Count activities in the last hour
|
||||
const hourAgo = now - 3600
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM activities WHERE created_at > ? AND ${safeColumn('activities', condition_field)} = ?`).get(hourAgo, condition_value) as any)?.c || 0
|
||||
const count = (db.prepare(`SELECT COUNT(*) as c FROM activities WHERE workspace_id = ? AND created_at > ? AND ${safeColumn('activities', condition_field)} = ?`).get(workspaceId, hourAgo, condition_value) as any)?.c || 0
|
||||
return count > parseInt(condition_value)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export async function POST(request: Request) {
|
|||
const avatar = profile.picture ? String(profile.picture) : null
|
||||
|
||||
const row = db.prepare(`
|
||||
SELECT id, username, display_name, role, provider, email, avatar_url, is_approved, created_at, updated_at, last_login_at
|
||||
SELECT id, username, display_name, role, provider, email, avatar_url, is_approved, created_at, updated_at, last_login_at, workspace_id
|
||||
FROM users
|
||||
WHERE (provider = 'google' AND provider_user_id = ?) OR lower(email) = ?
|
||||
ORDER BY id ASC
|
||||
|
|
@ -76,7 +76,7 @@ export async function POST(request: Request) {
|
|||
WHERE id = ?
|
||||
`).run(sub, email, avatar, row.id)
|
||||
|
||||
const { token, expiresAt } = createSession(row.id, ipAddress, userAgent)
|
||||
const { token, expiresAt } = createSession(row.id, ipAddress, userAgent, row.workspace_id ?? 1)
|
||||
|
||||
logAuditEvent({ action: 'login_google', actor: row.username, actor_id: row.id, ip_address: ipAddress, user_agent: userAgent })
|
||||
|
||||
|
|
@ -89,6 +89,7 @@ export async function POST(request: Request) {
|
|||
provider: 'google',
|
||||
email,
|
||||
avatar_url: avatar,
|
||||
workspace_id: row.workspace_id ?? 1,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export async function POST(request: Request) {
|
|||
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 })
|
||||
}
|
||||
|
||||
const { token, expiresAt } = createSession(user.id, ipAddress, userAgent)
|
||||
const { token, expiresAt } = createSession(user.id, ipAddress, userAgent, user.workspace_id)
|
||||
|
||||
logAuditEvent({ action: 'login', actor: user.username, actor_id: user.id, ip_address: ipAddress, user_agent: userAgent })
|
||||
|
||||
|
|
@ -38,6 +38,7 @@ export async function POST(request: Request) {
|
|||
provider: user.provider || 'local',
|
||||
email: user.email || null,
|
||||
avatar_url: user.avatar_url || null,
|
||||
workspace_id: user.workspace_id ?? 1,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getUserFromRequest, updateUser , requireRole } from '@/lib/auth'
|
||||
import { getUserFromRequest, updateUser, requireRole } from '@/lib/auth'
|
||||
import { logAuditEvent } from '@/lib/db'
|
||||
import { verifyPassword } from '@/lib/password'
|
||||
import { logger } from '@/lib/logger'
|
||||
|
|
@ -23,6 +23,7 @@ export async function GET(request: Request) {
|
|||
provider: user.provider || 'local',
|
||||
email: user.email || null,
|
||||
avatar_url: user.avatar_url || null,
|
||||
workspace_id: user.workspace_id ?? 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -103,6 +104,7 @@ export async function PATCH(request: NextRequest) {
|
|||
provider: updated.provider || 'local',
|
||||
email: updated.email || null,
|
||||
avatar_url: updated.avatar_url || null,
|
||||
workspace_id: updated.workspace_id ?? 1,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getUserFromRequest, getAllUsers, createUser, updateUser, deleteUser , requireRole } from '@/lib/auth'
|
||||
import { getUserFromRequest, getAllUsers, createUser, updateUser, deleteUser, getUserById, requireRole } from '@/lib/auth'
|
||||
import { logAuditEvent } from '@/lib/db'
|
||||
import { validateBody, createUserSchema } from '@/lib/validation'
|
||||
import { mutationLimiter } from '@/lib/rate-limit'
|
||||
|
|
@ -18,7 +18,8 @@ export async function GET(request: NextRequest) {
|
|||
}
|
||||
|
||||
const users = getAllUsers()
|
||||
return NextResponse.json({ users })
|
||||
const workspaceId = user.workspace_id ?? 1
|
||||
return NextResponse.json({ users: users.filter((u) => (u.workspace_id ?? 1) === workspaceId) })
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -38,7 +39,12 @@ export async function POST(request: NextRequest) {
|
|||
if ('error' in result) return result.error
|
||||
const { username, password, display_name, role, provider, email } = result.data
|
||||
|
||||
const newUser = createUser(username, password, display_name || username, role, { provider, email: email || null })
|
||||
const workspaceId = currentUser.workspace_id ?? 1
|
||||
const newUser = createUser(username, password, display_name || username, role, {
|
||||
provider,
|
||||
email: email || null,
|
||||
workspace_id: workspaceId,
|
||||
})
|
||||
|
||||
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
|
||||
logAuditEvent({
|
||||
|
|
@ -57,6 +63,7 @@ export async function POST(request: NextRequest) {
|
|||
email: newUser.email || null,
|
||||
avatar_url: newUser.avatar_url || null,
|
||||
is_approved: newUser.is_approved ?? 1,
|
||||
workspace_id: newUser.workspace_id ?? 1,
|
||||
}
|
||||
}, { status: 201 })
|
||||
} catch (error: any) {
|
||||
|
|
@ -79,8 +86,9 @@ export async function PUT(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const { id, display_name, role, password, is_approved, email, avatar_url } = await request.json()
|
||||
const userId = parseInt(String(id))
|
||||
|
||||
if (!id) {
|
||||
if (!id || Number.isNaN(userId)) {
|
||||
return NextResponse.json({ error: 'User ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
|
|
@ -89,11 +97,17 @@ export async function PUT(request: NextRequest) {
|
|||
}
|
||||
|
||||
// Prevent demoting yourself
|
||||
if (id === currentUser.id && role && role !== currentUser.role) {
|
||||
if (userId === currentUser.id && role && role !== currentUser.role) {
|
||||
return NextResponse.json({ error: 'Cannot change your own role' }, { status: 400 })
|
||||
}
|
||||
|
||||
const updated = updateUser(id, { display_name, role, password: password || undefined, is_approved, email, avatar_url })
|
||||
const workspaceId = currentUser.workspace_id ?? 1
|
||||
const existing = getUserById(userId)
|
||||
if (!existing || (existing.workspace_id ?? 1) !== workspaceId) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const updated = updateUser(userId, { display_name, role, password: password || undefined, is_approved, email, avatar_url })
|
||||
if (!updated) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
|
@ -101,7 +115,7 @@ export async function PUT(request: NextRequest) {
|
|||
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'unknown'
|
||||
logAuditEvent({
|
||||
action: 'user_update', actor: currentUser.username, actor_id: currentUser.id,
|
||||
target_type: 'user', target_id: id,
|
||||
target_type: 'user', target_id: userId,
|
||||
detail: { display_name, role, password_changed: !!password, is_approved }, ip_address: ipAddress,
|
||||
})
|
||||
|
||||
|
|
@ -115,6 +129,7 @@ export async function PUT(request: NextRequest) {
|
|||
email: updated.email || null,
|
||||
avatar_url: updated.avatar_url || null,
|
||||
is_approved: updated.is_approved ?? 1,
|
||||
workspace_id: updated.workspace_id ?? 1,
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
@ -147,6 +162,12 @@ export async function DELETE(request: NextRequest) {
|
|||
return NextResponse.json({ error: 'Cannot delete your own account' }, { status: 400 })
|
||||
}
|
||||
|
||||
const workspaceId = currentUser.workspace_id ?? 1
|
||||
const existing = getUserById(userId)
|
||||
if (!existing || (existing.workspace_id ?? 1) !== workspaceId) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const deleted = deleteUser(userId)
|
||||
if (!deleted) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const db = getDatabase()
|
||||
const { searchParams } = new URL(request.url)
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
|
||||
const agent = searchParams.get('agent')
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '50'), 200)
|
||||
|
|
@ -32,12 +33,12 @@ export async function GET(request: NextRequest) {
|
|||
COUNT(DISTINCT m.from_agent) + COUNT(DISTINCT CASE WHEN m.to_agent IS NOT NULL THEN m.to_agent END) as participant_count,
|
||||
SUM(CASE WHEN m.to_agent = ? AND m.read_at IS NULL THEN 1 ELSE 0 END) as unread_count
|
||||
FROM messages m
|
||||
WHERE m.from_agent = ? OR m.to_agent = ? OR m.to_agent IS NULL
|
||||
WHERE m.workspace_id = ? AND (m.from_agent = ? OR m.to_agent = ? OR m.to_agent IS NULL)
|
||||
GROUP BY m.conversation_id
|
||||
ORDER BY last_message_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
params.push(agent, agent, agent, limit, offset)
|
||||
params.push(agent, workspaceId, agent, agent, limit, offset)
|
||||
} else {
|
||||
query = `
|
||||
SELECT
|
||||
|
|
@ -47,11 +48,12 @@ export async function GET(request: NextRequest) {
|
|||
COUNT(DISTINCT m.from_agent) + COUNT(DISTINCT CASE WHEN m.to_agent IS NOT NULL THEN m.to_agent END) as participant_count,
|
||||
0 as unread_count
|
||||
FROM messages m
|
||||
WHERE m.workspace_id = ?
|
||||
GROUP BY m.conversation_id
|
||||
ORDER BY last_message_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`
|
||||
params.push(limit, offset)
|
||||
params.push(workspaceId, limit, offset)
|
||||
}
|
||||
|
||||
const conversations = db.prepare(query).all(...params) as any[]
|
||||
|
|
@ -59,13 +61,13 @@ export async function GET(request: NextRequest) {
|
|||
// Prepare last message statement once (avoids N+1)
|
||||
const lastMsgStmt = db.prepare(`
|
||||
SELECT * FROM messages
|
||||
WHERE conversation_id = ?
|
||||
WHERE conversation_id = ? AND workspace_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
const withLastMessage = conversations.map((conv) => {
|
||||
const lastMsg = lastMsgStmt.get(conv.conversation_id) as any;
|
||||
const lastMsg = lastMsgStmt.get(conv.conversation_id, workspaceId) as any;
|
||||
|
||||
return {
|
||||
...conv,
|
||||
|
|
@ -80,16 +82,16 @@ export async function GET(request: NextRequest) {
|
|||
|
||||
// Get total count for pagination
|
||||
let countQuery: string
|
||||
const countParams: any[] = []
|
||||
const countParams: any[] = [workspaceId]
|
||||
if (agent) {
|
||||
countQuery = `
|
||||
SELECT COUNT(DISTINCT m.conversation_id) as total
|
||||
FROM messages m
|
||||
WHERE m.from_agent = ? OR m.to_agent = ? OR m.to_agent IS NULL
|
||||
WHERE m.workspace_id = ? AND (m.from_agent = ? OR m.to_agent = ? OR m.to_agent IS NULL)
|
||||
`
|
||||
countParams.push(agent, agent)
|
||||
} else {
|
||||
countQuery = 'SELECT COUNT(DISTINCT conversation_id) as total FROM messages'
|
||||
countQuery = 'SELECT COUNT(DISTINCT conversation_id) as total FROM messages WHERE workspace_id = ?'
|
||||
}
|
||||
const countRow = db.prepare(countQuery).get(...countParams) as { total: number }
|
||||
|
||||
|
|
|
|||
|
|
@ -16,8 +16,11 @@ export async function GET(
|
|||
try {
|
||||
const db = getDatabase()
|
||||
const { id } = await params
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
|
||||
const message = db.prepare('SELECT * FROM messages WHERE id = ?').get(parseInt(id)) as Message | undefined
|
||||
const message = db
|
||||
.prepare('SELECT * FROM messages WHERE id = ? AND workspace_id = ?')
|
||||
.get(parseInt(id), workspaceId) as Message | undefined
|
||||
|
||||
if (!message) {
|
||||
return NextResponse.json({ error: 'Message not found' }, { status: 404 })
|
||||
|
|
@ -48,9 +51,12 @@ export async function PATCH(
|
|||
try {
|
||||
const db = getDatabase()
|
||||
const { id } = await params
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
const body = await request.json()
|
||||
|
||||
const message = db.prepare('SELECT * FROM messages WHERE id = ?').get(parseInt(id)) as Message | undefined
|
||||
const message = db
|
||||
.prepare('SELECT * FROM messages WHERE id = ? AND workspace_id = ?')
|
||||
.get(parseInt(id), workspaceId) as Message | undefined
|
||||
|
||||
if (!message) {
|
||||
return NextResponse.json({ error: 'Message not found' }, { status: 404 })
|
||||
|
|
@ -58,10 +64,12 @@ export async function PATCH(
|
|||
|
||||
if (body.read) {
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
db.prepare('UPDATE messages SET read_at = ? WHERE id = ?').run(now, parseInt(id))
|
||||
db.prepare('UPDATE messages SET read_at = ? WHERE id = ? AND workspace_id = ?').run(now, parseInt(id), workspaceId)
|
||||
}
|
||||
|
||||
const updated = db.prepare('SELECT * FROM messages WHERE id = ?').get(parseInt(id)) as Message
|
||||
const updated = db
|
||||
.prepare('SELECT * FROM messages WHERE id = ? AND workspace_id = ?')
|
||||
.get(parseInt(id), workspaceId) as Message
|
||||
|
||||
return NextResponse.json({
|
||||
message: {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ function parseGatewayJson(raw: string): any | null {
|
|||
|
||||
function createChatReply(
|
||||
db: ReturnType<typeof getDatabase>,
|
||||
workspaceId: number,
|
||||
conversationId: string,
|
||||
fromAgent: string,
|
||||
toAgent: string,
|
||||
|
|
@ -42,8 +43,8 @@ function createChatReply(
|
|||
) {
|
||||
const replyInsert = db
|
||||
.prepare(`
|
||||
INSERT INTO messages (conversation_id, from_agent, to_agent, content, message_type, metadata)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO messages (conversation_id, from_agent, to_agent, content, message_type, metadata, workspace_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
.run(
|
||||
conversationId,
|
||||
|
|
@ -51,12 +52,13 @@ function createChatReply(
|
|||
toAgent,
|
||||
content,
|
||||
messageType,
|
||||
metadata ? JSON.stringify(metadata) : null
|
||||
metadata ? JSON.stringify(metadata) : null,
|
||||
workspaceId
|
||||
)
|
||||
|
||||
const row = db
|
||||
.prepare('SELECT * FROM messages WHERE id = ?')
|
||||
.get(replyInsert.lastInsertRowid) as Message
|
||||
.prepare('SELECT * FROM messages WHERE id = ? AND workspace_id = ?')
|
||||
.get(replyInsert.lastInsertRowid, workspaceId) as Message
|
||||
|
||||
eventBus.broadcast('chat.message', {
|
||||
...row,
|
||||
|
|
@ -102,6 +104,7 @@ export async function GET(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
const { searchParams } = new URL(request.url)
|
||||
|
||||
const conversation_id = searchParams.get('conversation_id')
|
||||
|
|
@ -111,8 +114,8 @@ export async function GET(request: NextRequest) {
|
|||
const offset = parseInt(searchParams.get('offset') || '0')
|
||||
const since = searchParams.get('since')
|
||||
|
||||
let query = 'SELECT * FROM messages WHERE 1=1'
|
||||
const params: any[] = []
|
||||
let query = 'SELECT * FROM messages WHERE workspace_id = ?'
|
||||
const params: any[] = [workspaceId]
|
||||
|
||||
if (conversation_id) {
|
||||
query += ' AND conversation_id = ?'
|
||||
|
|
@ -145,8 +148,8 @@ export async function GET(request: NextRequest) {
|
|||
}))
|
||||
|
||||
// Get total count for pagination
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM messages WHERE 1=1'
|
||||
const countParams: any[] = []
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM messages WHERE workspace_id = ?'
|
||||
const countParams: any[] = [workspaceId]
|
||||
if (conversation_id) {
|
||||
countQuery += ' AND conversation_id = ?'
|
||||
countParams.push(conversation_id)
|
||||
|
|
@ -182,6 +185,7 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
const body = await request.json()
|
||||
|
||||
const from = (body.from || '').trim()
|
||||
|
|
@ -199,8 +203,8 @@ export async function POST(request: NextRequest) {
|
|||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO messages (conversation_id, from_agent, to_agent, content, message_type, metadata)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO messages (conversation_id, from_agent, to_agent, content, message_type, metadata, workspace_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
const result = stmt.run(
|
||||
|
|
@ -209,7 +213,8 @@ export async function POST(request: NextRequest) {
|
|||
to,
|
||||
content,
|
||||
message_type,
|
||||
metadata ? JSON.stringify(metadata) : null
|
||||
metadata ? JSON.stringify(metadata) : null,
|
||||
workspaceId
|
||||
)
|
||||
|
||||
const messageId = result.lastInsertRowid as number
|
||||
|
|
@ -223,7 +228,8 @@ export async function POST(request: NextRequest) {
|
|||
messageId,
|
||||
from,
|
||||
`Sent ${message_type} message${to ? ` to ${to}` : ' (broadcast)'}`,
|
||||
{ conversation_id, to, message_type }
|
||||
{ conversation_id, to, message_type },
|
||||
workspaceId
|
||||
)
|
||||
|
||||
// Create notification for recipient if specified
|
||||
|
|
@ -234,7 +240,8 @@ export async function POST(request: NextRequest) {
|
|||
`Message from ${from}`,
|
||||
content.substring(0, 200) + (content.length > 200 ? '...' : ''),
|
||||
'message',
|
||||
messageId
|
||||
messageId,
|
||||
workspaceId
|
||||
)
|
||||
|
||||
// Optionally forward to agent via gateway
|
||||
|
|
@ -242,8 +249,8 @@ export async function POST(request: NextRequest) {
|
|||
forwardInfo = { attempted: true, delivered: false }
|
||||
|
||||
const agent = db
|
||||
.prepare('SELECT * FROM agents WHERE lower(name) = lower(?)')
|
||||
.get(to) as any
|
||||
.prepare('SELECT * FROM agents WHERE lower(name) = lower(?) AND workspace_id = ?')
|
||||
.get(to, workspaceId) as any
|
||||
|
||||
let sessionKey: string | null = agent?.session_key || null
|
||||
|
||||
|
|
@ -280,6 +287,7 @@ export async function POST(request: NextRequest) {
|
|||
try {
|
||||
createChatReply(
|
||||
db,
|
||||
workspaceId,
|
||||
conversation_id,
|
||||
COORDINATOR_AGENT,
|
||||
from,
|
||||
|
|
@ -340,6 +348,7 @@ export async function POST(request: NextRequest) {
|
|||
try {
|
||||
createChatReply(
|
||||
db,
|
||||
workspaceId,
|
||||
conversation_id,
|
||||
COORDINATOR_AGENT,
|
||||
from,
|
||||
|
|
@ -363,6 +372,7 @@ export async function POST(request: NextRequest) {
|
|||
try {
|
||||
createChatReply(
|
||||
db,
|
||||
workspaceId,
|
||||
conversation_id,
|
||||
COORDINATOR_AGENT,
|
||||
from,
|
||||
|
|
@ -401,6 +411,7 @@ export async function POST(request: NextRequest) {
|
|||
: 'Unknown runtime error'
|
||||
createChatReply(
|
||||
db,
|
||||
workspaceId,
|
||||
conversation_id,
|
||||
COORDINATOR_AGENT,
|
||||
from,
|
||||
|
|
@ -411,6 +422,7 @@ export async function POST(request: NextRequest) {
|
|||
} else if (waitStatus === 'timeout') {
|
||||
createChatReply(
|
||||
db,
|
||||
workspaceId,
|
||||
conversation_id,
|
||||
COORDINATOR_AGENT,
|
||||
from,
|
||||
|
|
@ -423,6 +435,7 @@ export async function POST(request: NextRequest) {
|
|||
if (replyText) {
|
||||
createChatReply(
|
||||
db,
|
||||
workspaceId,
|
||||
conversation_id,
|
||||
COORDINATOR_AGENT,
|
||||
from,
|
||||
|
|
@ -433,6 +446,7 @@ export async function POST(request: NextRequest) {
|
|||
} else {
|
||||
createChatReply(
|
||||
db,
|
||||
workspaceId,
|
||||
conversation_id,
|
||||
COORDINATOR_AGENT,
|
||||
from,
|
||||
|
|
@ -453,6 +467,7 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
createChatReply(
|
||||
db,
|
||||
workspaceId,
|
||||
conversation_id,
|
||||
COORDINATOR_AGENT,
|
||||
from,
|
||||
|
|
@ -467,7 +482,7 @@ export async function POST(request: NextRequest) {
|
|||
}
|
||||
}
|
||||
|
||||
const created = db.prepare('SELECT * FROM messages WHERE id = ?').get(messageId) as Message
|
||||
const created = db.prepare('SELECT * FROM messages WHERE id = ? AND workspace_id = ?').get(messageId, workspaceId) as Message
|
||||
const parsedMessage = {
|
||||
...created,
|
||||
metadata: created.metadata ? JSON.parse(created.metadata) : null
|
||||
|
|
|
|||
|
|
@ -21,22 +21,23 @@ export async function POST(request: NextRequest) {
|
|||
const { tool_name, tool_version, agent_name, agent_role, metadata } = validation.data
|
||||
const db = getDatabase()
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
// Find or create agent
|
||||
let agent = db.prepare('SELECT * FROM agents WHERE name = ?').get(agent_name) as any
|
||||
let agent = db.prepare('SELECT * FROM agents WHERE name = ? AND workspace_id = ?').get(agent_name, workspaceId) as any
|
||||
if (!agent) {
|
||||
const result = db.prepare(
|
||||
`INSERT INTO agents (name, role, status, created_at, updated_at)
|
||||
VALUES (?, ?, 'online', ?, ?)`
|
||||
).run(agent_name, agent_role || 'cli', now, now)
|
||||
`INSERT INTO agents (name, role, status, created_at, updated_at, workspace_id)
|
||||
VALUES (?, ?, 'online', ?, ?, ?)`
|
||||
).run(agent_name, agent_role || 'cli', now, now, workspaceId)
|
||||
agent = { id: result.lastInsertRowid, name: agent_name }
|
||||
db_helpers.logActivity('agent_created', 'agent', agent.id as number, 'system',
|
||||
`Auto-created agent "${agent_name}" via direct CLI connection`)
|
||||
`Auto-created agent "${agent_name}" via direct CLI connection`, undefined, workspaceId)
|
||||
eventBus.broadcast('agent.created', { id: agent.id, name: agent_name })
|
||||
} else {
|
||||
// Set agent online
|
||||
db.prepare('UPDATE agents SET status = ?, updated_at = ? WHERE id = ?')
|
||||
.run('online', now, agent.id)
|
||||
db.prepare('UPDATE agents SET status = ?, updated_at = ? WHERE id = ? AND workspace_id = ?')
|
||||
.run('online', now, agent.id, workspaceId)
|
||||
eventBus.broadcast('agent.status_changed', { id: agent.id, name: agent.name, status: 'online' })
|
||||
}
|
||||
|
||||
|
|
@ -48,12 +49,12 @@ export async function POST(request: NextRequest) {
|
|||
// Create new connection
|
||||
const connectionId = randomUUID()
|
||||
db.prepare(
|
||||
`INSERT INTO direct_connections (agent_id, tool_name, tool_version, connection_id, status, last_heartbeat, metadata, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 'connected', ?, ?, ?, ?)`
|
||||
).run(agent.id, tool_name, tool_version || null, connectionId, now, metadata ? JSON.stringify(metadata) : null, now, now)
|
||||
`INSERT INTO direct_connections (agent_id, tool_name, tool_version, connection_id, status, last_heartbeat, metadata, created_at, updated_at, workspace_id)
|
||||
VALUES (?, ?, ?, ?, 'connected', ?, ?, ?, ?, ?)`
|
||||
).run(agent.id, tool_name, tool_version || null, connectionId, now, metadata ? JSON.stringify(metadata) : null, now, now, workspaceId)
|
||||
|
||||
db_helpers.logActivity('connection_created', 'agent', agent.id as number, agent_name,
|
||||
`CLI connection established via ${tool_name}${tool_version ? ` v${tool_version}` : ''}`)
|
||||
`CLI connection established via ${tool_name}${tool_version ? ` v${tool_version}` : ''}`, undefined, workspaceId)
|
||||
|
||||
eventBus.broadcast('connection.created', {
|
||||
connection_id: connectionId,
|
||||
|
|
@ -81,12 +82,14 @@ export async function GET(request: NextRequest) {
|
|||
if ('error' in auth) return NextResponse.json({ error: auth.error }, { status: auth.status })
|
||||
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const connections = db.prepare(`
|
||||
SELECT dc.*, a.name as agent_name, a.status as agent_status, a.role as agent_role
|
||||
FROM direct_connections dc
|
||||
JOIN agents a ON dc.agent_id = a.id
|
||||
WHERE a.workspace_id = ?
|
||||
ORDER BY dc.created_at DESC
|
||||
`).all()
|
||||
`).all(workspaceId)
|
||||
|
||||
return NextResponse.json({ connections })
|
||||
}
|
||||
|
|
@ -112,8 +115,14 @@ export async function DELETE(request: NextRequest) {
|
|||
|
||||
const db = getDatabase()
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
const conn = db.prepare('SELECT * FROM direct_connections WHERE connection_id = ?').get(connection_id) as any
|
||||
const conn = db.prepare(`
|
||||
SELECT dc.*
|
||||
FROM direct_connections dc
|
||||
JOIN agents a ON a.id = dc.agent_id
|
||||
WHERE dc.connection_id = ? AND a.workspace_id = ?
|
||||
`).get(connection_id, workspaceId) as any
|
||||
if (!conn) {
|
||||
return NextResponse.json({ error: 'Connection not found' }, { status: 404 })
|
||||
}
|
||||
|
|
@ -126,13 +135,13 @@ export async function DELETE(request: NextRequest) {
|
|||
'SELECT COUNT(*) as count FROM direct_connections WHERE agent_id = ? AND status = ? AND connection_id != ?'
|
||||
).get(conn.agent_id, 'connected', connection_id) as any
|
||||
if (!otherActive?.count) {
|
||||
db.prepare('UPDATE agents SET status = ?, updated_at = ? WHERE id = ?')
|
||||
.run('offline', now, conn.agent_id)
|
||||
db.prepare('UPDATE agents SET status = ?, updated_at = ? WHERE id = ? AND workspace_id = ?')
|
||||
.run('offline', now, conn.agent_id, workspaceId)
|
||||
}
|
||||
|
||||
const agent = db.prepare('SELECT name FROM agents WHERE id = ?').get(conn.agent_id) as any
|
||||
const agent = db.prepare('SELECT name FROM agents WHERE id = ? AND workspace_id = ?').get(conn.agent_id, workspaceId) as any
|
||||
db_helpers.logActivity('connection_disconnected', 'agent', conn.agent_id, agent?.name || 'unknown',
|
||||
`CLI connection disconnected (${conn.tool_name})`)
|
||||
`CLI connection disconnected (${conn.tool_name})`, undefined, workspaceId)
|
||||
|
||||
eventBus.broadcast('connection.disconnected', {
|
||||
connection_id,
|
||||
|
|
|
|||
|
|
@ -308,7 +308,7 @@ export async function POST(request: NextRequest) {
|
|||
}
|
||||
|
||||
if (action === 'add') {
|
||||
const { schedule, command, description } = body
|
||||
const { schedule, command, model, description } = body
|
||||
const name = jobName || body.name
|
||||
if (!schedule || !command || !name) {
|
||||
return NextResponse.json(
|
||||
|
|
@ -336,6 +336,7 @@ export async function POST(request: NextRequest) {
|
|||
payload: {
|
||||
kind: 'agentTurn',
|
||||
message: command,
|
||||
...(typeof model === 'string' && model.trim() ? { model: model.trim() } : {}),
|
||||
},
|
||||
delivery: {
|
||||
mode: 'none',
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export async function GET(request: NextRequest) {
|
|||
}
|
||||
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
const conditions: string[] = []
|
||||
const params: any[] = []
|
||||
|
||||
|
|
@ -58,13 +59,19 @@ export async function GET(request: NextRequest) {
|
|||
break
|
||||
}
|
||||
case 'tasks': {
|
||||
rows = db.prepare(`SELECT * FROM tasks ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit)
|
||||
conditions.unshift('workspace_id = ?')
|
||||
params.unshift(workspaceId)
|
||||
const scopedWhere = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
||||
rows = db.prepare(`SELECT * FROM tasks ${scopedWhere} ORDER BY created_at DESC LIMIT ?`).all(...params, limit)
|
||||
headers = ['id', 'title', 'description', 'status', 'priority', 'assigned_to', 'created_by', 'created_at', 'updated_at', 'due_date', 'estimated_hours', 'actual_hours', 'tags']
|
||||
filename = 'tasks'
|
||||
break
|
||||
}
|
||||
case 'activities': {
|
||||
rows = db.prepare(`SELECT * FROM activities ${where} ORDER BY created_at DESC LIMIT ?`).all(...params, limit)
|
||||
conditions.unshift('workspace_id = ?')
|
||||
params.unshift(workspaceId)
|
||||
const scopedWhere = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
||||
rows = db.prepare(`SELECT * FROM activities ${scopedWhere} ORDER BY created_at DESC LIMIT ?`).all(...params, limit)
|
||||
headers = ['id', 'type', 'entity_type', 'entity_id', 'actor', 'description', 'data', 'created_at']
|
||||
filename = 'activities'
|
||||
break
|
||||
|
|
|
|||
|
|
@ -76,13 +76,13 @@ export async function POST(request: NextRequest) {
|
|||
try {
|
||||
switch (action) {
|
||||
case 'sync':
|
||||
return await handleSync(body, auth.user.username)
|
||||
return await handleSync(body, auth.user.username, auth.user.workspace_id ?? 1)
|
||||
case 'comment':
|
||||
return await handleComment(body, auth.user.username)
|
||||
return await handleComment(body, auth.user.username, auth.user.workspace_id ?? 1)
|
||||
case 'close':
|
||||
return await handleClose(body, auth.user.username)
|
||||
return await handleClose(body, auth.user.username, auth.user.workspace_id ?? 1)
|
||||
case 'status':
|
||||
return handleStatus()
|
||||
return handleStatus(auth.user.workspace_id ?? 1)
|
||||
default:
|
||||
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
|
||||
}
|
||||
|
|
@ -96,7 +96,8 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
async function handleSync(
|
||||
body: { repo?: string; labels?: string; state?: 'open' | 'closed' | 'all'; assignAgent?: string },
|
||||
actor: string
|
||||
actor: string,
|
||||
workspaceId: number
|
||||
) {
|
||||
const repo = body.repo || process.env.GITHUB_DEFAULT_REPO
|
||||
if (!repo) {
|
||||
|
|
@ -128,7 +129,8 @@ async function handleSync(
|
|||
SELECT id FROM tasks
|
||||
WHERE json_extract(metadata, '$.github_repo') = ?
|
||||
AND json_extract(metadata, '$.github_issue_number') = ?
|
||||
`).get(repo, issue.number) as { id: number } | undefined
|
||||
AND workspace_id = ?
|
||||
`).get(repo, issue.number, workspaceId) as { id: number } | undefined
|
||||
|
||||
if (existing) {
|
||||
skipped++
|
||||
|
|
@ -151,8 +153,8 @@ async function handleSync(
|
|||
const stmt = db.prepare(`
|
||||
INSERT INTO tasks (
|
||||
title, description, status, priority, assigned_to, created_by,
|
||||
created_at, updated_at, tags, metadata
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
created_at, updated_at, tags, metadata, workspace_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
|
||||
const dbResult = stmt.run(
|
||||
|
|
@ -165,7 +167,8 @@ async function handleSync(
|
|||
now,
|
||||
now,
|
||||
JSON.stringify(tags),
|
||||
JSON.stringify(metadata)
|
||||
JSON.stringify(metadata),
|
||||
workspaceId
|
||||
)
|
||||
|
||||
const taskId = dbResult.lastInsertRowid as number
|
||||
|
|
@ -176,10 +179,11 @@ async function handleSync(
|
|||
taskId,
|
||||
actor,
|
||||
`Imported from GitHub: ${repo}#${issue.number}`,
|
||||
{ github_issue: issue.number, github_repo: repo }
|
||||
{ github_issue: issue.number, github_repo: repo },
|
||||
workspaceId
|
||||
)
|
||||
|
||||
const createdTask = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) as Task
|
||||
const createdTask = db.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?').get(taskId, workspaceId) as Task
|
||||
const parsedTask = {
|
||||
...createdTask,
|
||||
tags: JSON.parse(createdTask.tags || '[]'),
|
||||
|
|
@ -196,16 +200,33 @@ async function handleSync(
|
|||
}
|
||||
|
||||
// Log sync to github_syncs table
|
||||
db.prepare(`
|
||||
INSERT INTO github_syncs (repo, last_synced_at, issue_count, sync_direction, status, error)
|
||||
VALUES (?, ?, ?, 'inbound', ?, ?)
|
||||
`).run(
|
||||
repo,
|
||||
now,
|
||||
imported,
|
||||
errors > 0 ? 'partial' : 'success',
|
||||
errors > 0 ? `${errors} issues failed to import` : null
|
||||
)
|
||||
const syncTableHasWorkspace = db
|
||||
.prepare("SELECT 1 as ok FROM pragma_table_info('github_syncs') WHERE name = 'workspace_id'")
|
||||
.get() as { ok?: number } | undefined
|
||||
if (syncTableHasWorkspace?.ok) {
|
||||
db.prepare(`
|
||||
INSERT INTO github_syncs (repo, last_synced_at, issue_count, sync_direction, status, error, workspace_id)
|
||||
VALUES (?, ?, ?, 'inbound', ?, ?, ?)
|
||||
`).run(
|
||||
repo,
|
||||
now,
|
||||
imported,
|
||||
errors > 0 ? 'partial' : 'success',
|
||||
errors > 0 ? `${errors} issues failed to import` : null,
|
||||
workspaceId
|
||||
)
|
||||
} else {
|
||||
db.prepare(`
|
||||
INSERT INTO github_syncs (repo, last_synced_at, issue_count, sync_direction, status, error)
|
||||
VALUES (?, ?, ?, 'inbound', ?, ?)
|
||||
`).run(
|
||||
repo,
|
||||
now,
|
||||
imported,
|
||||
errors > 0 ? 'partial' : 'success',
|
||||
errors > 0 ? `${errors} issues failed to import` : null
|
||||
)
|
||||
}
|
||||
|
||||
eventBus.broadcast('github.synced', {
|
||||
repo,
|
||||
|
|
@ -227,7 +248,8 @@ async function handleSync(
|
|||
|
||||
async function handleComment(
|
||||
body: { repo?: string; issueNumber?: number; body?: string },
|
||||
actor: string
|
||||
actor: string,
|
||||
workspaceId: number
|
||||
) {
|
||||
if (!body.repo || !body.issueNumber || !body.body) {
|
||||
return NextResponse.json(
|
||||
|
|
@ -244,7 +266,8 @@ async function handleComment(
|
|||
0,
|
||||
actor,
|
||||
`Commented on ${body.repo}#${body.issueNumber}`,
|
||||
{ github_repo: body.repo, github_issue: body.issueNumber }
|
||||
{ github_repo: body.repo, github_issue: body.issueNumber },
|
||||
workspaceId
|
||||
)
|
||||
|
||||
return NextResponse.json({ ok: true })
|
||||
|
|
@ -254,7 +277,8 @@ async function handleComment(
|
|||
|
||||
async function handleClose(
|
||||
body: { repo?: string; issueNumber?: number; comment?: string },
|
||||
actor: string
|
||||
actor: string,
|
||||
workspaceId: number
|
||||
) {
|
||||
if (!body.repo || !body.issueNumber) {
|
||||
return NextResponse.json(
|
||||
|
|
@ -279,7 +303,8 @@ async function handleClose(
|
|||
updated_at = ?
|
||||
WHERE json_extract(metadata, '$.github_repo') = ?
|
||||
AND json_extract(metadata, '$.github_issue_number') = ?
|
||||
`).run(now, body.repo, body.issueNumber)
|
||||
AND workspace_id = ?
|
||||
`).run(now, body.repo, body.issueNumber, workspaceId)
|
||||
|
||||
db_helpers.logActivity(
|
||||
'github_close',
|
||||
|
|
@ -287,7 +312,8 @@ async function handleClose(
|
|||
0,
|
||||
actor,
|
||||
`Closed GitHub issue ${body.repo}#${body.issueNumber}`,
|
||||
{ github_repo: body.repo, github_issue: body.issueNumber }
|
||||
{ github_repo: body.repo, github_issue: body.issueNumber },
|
||||
workspaceId
|
||||
)
|
||||
|
||||
return NextResponse.json({ ok: true })
|
||||
|
|
@ -295,13 +321,17 @@ async function handleClose(
|
|||
|
||||
// ── Status: return recent sync history ──────────────────────────
|
||||
|
||||
function handleStatus() {
|
||||
function handleStatus(workspaceId: number) {
|
||||
const db = getDatabase()
|
||||
const tableHasWorkspace = db
|
||||
.prepare("SELECT 1 as ok FROM pragma_table_info('github_syncs') WHERE name = 'workspace_id'")
|
||||
.get() as { ok?: number } | undefined
|
||||
const syncs = db.prepare(`
|
||||
SELECT * FROM github_syncs
|
||||
${tableHasWorkspace?.ok ? 'WHERE workspace_id = ?' : ''}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 20
|
||||
`).all()
|
||||
`).all(...(tableHasWorkspace?.ok ? [workspaceId] : []))
|
||||
|
||||
return NextResponse.json({ syncs })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export async function POST(request: NextRequest) {
|
|||
try {
|
||||
const db = getDatabase();
|
||||
const body = await request.json();
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const {
|
||||
agent_filter, // Optional: only deliver to specific agent
|
||||
limit = 50, // Max notifications to process per call
|
||||
|
|
@ -27,11 +28,11 @@ export async function POST(request: NextRequest) {
|
|||
let query = `
|
||||
SELECT n.*, a.session_key
|
||||
FROM notifications n
|
||||
LEFT JOIN agents a ON n.recipient = a.name
|
||||
WHERE n.delivered_at IS NULL
|
||||
LEFT JOIN agents a ON n.recipient = a.name AND a.workspace_id = n.workspace_id
|
||||
WHERE n.delivered_at IS NULL AND n.workspace_id = ?
|
||||
`;
|
||||
|
||||
const params: any[] = [];
|
||||
const params: any[] = [workspaceId];
|
||||
|
||||
if (agent_filter) {
|
||||
query += ' AND n.recipient = ?';
|
||||
|
|
@ -59,7 +60,7 @@ export async function POST(request: NextRequest) {
|
|||
const deliveryResults: any[] = [];
|
||||
|
||||
// Prepare update statement once (avoids N+1)
|
||||
const markDeliveredStmt = db.prepare('UPDATE notifications SET delivered_at = ? WHERE id = ?');
|
||||
const markDeliveredStmt = db.prepare('UPDATE notifications SET delivered_at = ? WHERE id = ? AND workspace_id = ?');
|
||||
|
||||
for (const notification of undeliveredNotifications) {
|
||||
try {
|
||||
|
|
@ -98,7 +99,7 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
// Mark as delivered
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
markDeliveredStmt.run(now, notification.id);
|
||||
markDeliveredStmt.run(now, notification.id, workspaceId);
|
||||
|
||||
deliveredCount++;
|
||||
deliveryResults.push({
|
||||
|
|
@ -121,7 +122,8 @@ export async function POST(request: NextRequest) {
|
|||
notification_type: notification.type,
|
||||
session_key: notification.session_key,
|
||||
title: notification.title
|
||||
}
|
||||
},
|
||||
workspaceId
|
||||
);
|
||||
} catch (cmdError: any) {
|
||||
throw new Error(`Command failed: ${cmdError.message}`);
|
||||
|
|
@ -162,7 +164,8 @@ export async function POST(request: NextRequest) {
|
|||
errors: errorCount,
|
||||
dry_run,
|
||||
agent_filter: agent_filter || null
|
||||
}
|
||||
},
|
||||
workspaceId
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
|
|
@ -191,25 +194,26 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const db = getDatabase();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const agent = searchParams.get('agent');
|
||||
|
||||
// Get delivery statistics
|
||||
let baseQuery = 'SELECT COUNT(*) as count FROM notifications';
|
||||
let params: any[] = [];
|
||||
let baseQuery = 'SELECT COUNT(*) as count FROM notifications WHERE workspace_id = ?';
|
||||
let params: any[] = [workspaceId];
|
||||
|
||||
if (agent) {
|
||||
baseQuery += ' WHERE recipient = ?';
|
||||
baseQuery += ' AND recipient = ?';
|
||||
params.push(agent);
|
||||
}
|
||||
|
||||
const totalNotifications = db.prepare(baseQuery).get(...params) as { count: number };
|
||||
|
||||
const undeliveredCount = db.prepare(
|
||||
baseQuery + (agent ? ' AND' : ' WHERE') + ' delivered_at IS NULL'
|
||||
baseQuery + ' AND delivered_at IS NULL'
|
||||
).get(...params) as { count: number };
|
||||
|
||||
const deliveredCount = db.prepare(
|
||||
baseQuery + (agent ? ' AND' : ' WHERE') + ' delivered_at IS NOT NULL'
|
||||
baseQuery + ' AND delivered_at IS NOT NULL'
|
||||
).get(...params) as { count: number };
|
||||
|
||||
// Get recent delivery activity
|
||||
|
|
@ -221,11 +225,11 @@ export async function GET(request: NextRequest) {
|
|||
delivered_at,
|
||||
created_at
|
||||
FROM notifications
|
||||
WHERE delivered_at IS NOT NULL
|
||||
WHERE delivered_at IS NOT NULL AND workspace_id = ?
|
||||
${agent ? 'AND recipient = ?' : ''}
|
||||
ORDER BY delivered_at DESC
|
||||
LIMIT 10
|
||||
`).all(...(agent ? [agent] : []));
|
||||
`).all(...(agent ? [workspaceId, agent] : [workspaceId]));
|
||||
|
||||
// Get agents with pending notifications
|
||||
const agentsPending = db.prepare(`
|
||||
|
|
@ -234,11 +238,11 @@ export async function GET(request: NextRequest) {
|
|||
a.session_key,
|
||||
COUNT(*) as pending_count
|
||||
FROM notifications n
|
||||
LEFT JOIN agents a ON n.recipient = a.name
|
||||
WHERE n.delivered_at IS NULL
|
||||
LEFT JOIN agents a ON n.recipient = a.name AND a.workspace_id = n.workspace_id
|
||||
WHERE n.delivered_at IS NULL AND n.workspace_id = ?
|
||||
GROUP BY n.recipient, a.session_key
|
||||
ORDER BY pending_count DESC
|
||||
`).all() as any[];
|
||||
`).all(workspaceId) as any[];
|
||||
|
||||
return NextResponse.json({
|
||||
statistics: {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const db = getDatabase();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
// Parse query parameters
|
||||
const recipient = searchParams.get('recipient');
|
||||
|
|
@ -29,8 +30,8 @@ export async function GET(request: NextRequest) {
|
|||
}
|
||||
|
||||
// Build dynamic query
|
||||
let query = 'SELECT * FROM notifications WHERE recipient = ?';
|
||||
const params: any[] = [recipient];
|
||||
let query = 'SELECT * FROM notifications WHERE recipient = ? AND workspace_id = ?';
|
||||
const params: any[] = [recipient, workspaceId];
|
||||
|
||||
if (unread_only) {
|
||||
query += ' AND read_at IS NULL';
|
||||
|
|
@ -48,14 +49,14 @@ export async function GET(request: NextRequest) {
|
|||
const notifications = stmt.all(...params) as Notification[];
|
||||
|
||||
// Prepare source detail statements once (avoids N+1)
|
||||
const taskDetailStmt = db.prepare('SELECT id, title, status FROM tasks WHERE id = ?');
|
||||
const taskDetailStmt = db.prepare('SELECT id, title, status FROM tasks WHERE id = ? AND workspace_id = ?');
|
||||
const commentDetailStmt = db.prepare(`
|
||||
SELECT c.id, c.content, c.task_id, t.title as task_title
|
||||
FROM comments c
|
||||
LEFT JOIN tasks t ON c.task_id = t.id
|
||||
WHERE c.id = ?
|
||||
WHERE c.id = ? AND c.workspace_id = ? AND t.workspace_id = ?
|
||||
`);
|
||||
const agentDetailStmt = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ?');
|
||||
const agentDetailStmt = db.prepare('SELECT id, name, role, status FROM agents WHERE id = ? AND workspace_id = ?');
|
||||
|
||||
// Enhance notifications with related entity data
|
||||
const enhancedNotifications = notifications.map(notification => {
|
||||
|
|
@ -65,14 +66,14 @@ export async function GET(request: NextRequest) {
|
|||
if (notification.source_type && notification.source_id) {
|
||||
switch (notification.source_type) {
|
||||
case 'task': {
|
||||
const task = taskDetailStmt.get(notification.source_id) as any;
|
||||
const task = taskDetailStmt.get(notification.source_id, workspaceId) as any;
|
||||
if (task) {
|
||||
sourceDetails = { type: 'task', ...task };
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'comment': {
|
||||
const comment = commentDetailStmt.get(notification.source_id) as any;
|
||||
const comment = commentDetailStmt.get(notification.source_id, workspaceId, workspaceId) as any;
|
||||
if (comment) {
|
||||
sourceDetails = {
|
||||
type: 'comment',
|
||||
|
|
@ -83,7 +84,7 @@ export async function GET(request: NextRequest) {
|
|||
break;
|
||||
}
|
||||
case 'agent': {
|
||||
const agent = agentDetailStmt.get(notification.source_id) as any;
|
||||
const agent = agentDetailStmt.get(notification.source_id, workspaceId) as any;
|
||||
if (agent) {
|
||||
sourceDetails = { type: 'agent', ...agent };
|
||||
}
|
||||
|
|
@ -105,12 +106,12 @@ export async function GET(request: NextRequest) {
|
|||
const unreadCount = db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM notifications
|
||||
WHERE recipient = ? AND read_at IS NULL
|
||||
`).get(recipient) as { count: number };
|
||||
WHERE recipient = ? AND read_at IS NULL AND workspace_id = ?
|
||||
`).get(recipient, workspaceId) as { count: number };
|
||||
|
||||
// Get total count for pagination
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM notifications WHERE recipient = ?';
|
||||
const countParams: any[] = [recipient];
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM notifications WHERE recipient = ? AND workspace_id = ?';
|
||||
const countParams: any[] = [recipient, workspaceId];
|
||||
if (unread_only) {
|
||||
countQuery += ' AND read_at IS NULL';
|
||||
}
|
||||
|
|
@ -146,6 +147,7 @@ export async function PUT(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const body = await request.json();
|
||||
const { ids, recipient, markAllRead } = body;
|
||||
|
||||
|
|
@ -156,10 +158,10 @@ export async function PUT(request: NextRequest) {
|
|||
const stmt = db.prepare(`
|
||||
UPDATE notifications
|
||||
SET read_at = ?
|
||||
WHERE recipient = ? AND read_at IS NULL
|
||||
WHERE recipient = ? AND read_at IS NULL AND workspace_id = ?
|
||||
`);
|
||||
|
||||
const result = stmt.run(now, recipient);
|
||||
const result = stmt.run(now, recipient, workspaceId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
|
@ -171,10 +173,10 @@ export async function PUT(request: NextRequest) {
|
|||
const stmt = db.prepare(`
|
||||
UPDATE notifications
|
||||
SET read_at = ?
|
||||
WHERE id IN (${placeholders}) AND read_at IS NULL
|
||||
WHERE id IN (${placeholders}) AND read_at IS NULL AND workspace_id = ?
|
||||
`);
|
||||
|
||||
const result = stmt.run(now, ...ids);
|
||||
const result = stmt.run(now, ...ids, workspaceId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
|
@ -204,6 +206,7 @@ export async function DELETE(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const body = await request.json();
|
||||
const { ids, recipient, olderThan } = body;
|
||||
|
||||
|
|
@ -212,10 +215,10 @@ export async function DELETE(request: NextRequest) {
|
|||
const placeholders = ids.map(() => '?').join(',');
|
||||
const stmt = db.prepare(`
|
||||
DELETE FROM notifications
|
||||
WHERE id IN (${placeholders})
|
||||
WHERE id IN (${placeholders}) AND workspace_id = ?
|
||||
`);
|
||||
|
||||
const result = stmt.run(...ids);
|
||||
const result = stmt.run(...ids, workspaceId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
|
@ -225,10 +228,10 @@ export async function DELETE(request: NextRequest) {
|
|||
// Delete old notifications for recipient
|
||||
const stmt = db.prepare(`
|
||||
DELETE FROM notifications
|
||||
WHERE recipient = ? AND created_at < ?
|
||||
WHERE recipient = ? AND created_at < ? AND workspace_id = ?
|
||||
`);
|
||||
|
||||
const result = stmt.run(recipient, olderThan);
|
||||
const result = stmt.run(recipient, olderThan, workspaceId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
|
@ -258,6 +261,7 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
const result = await validateBody(request, notificationActionSchema);
|
||||
if ('error' in result) return result.error;
|
||||
|
|
@ -271,17 +275,17 @@ export async function POST(request: NextRequest) {
|
|||
const stmt = db.prepare(`
|
||||
UPDATE notifications
|
||||
SET delivered_at = ?
|
||||
WHERE recipient = ? AND delivered_at IS NULL
|
||||
WHERE recipient = ? AND delivered_at IS NULL AND workspace_id = ?
|
||||
`);
|
||||
|
||||
const result = stmt.run(now, agent);
|
||||
const result = stmt.run(now, agent, workspaceId);
|
||||
|
||||
// Get the notifications that were just marked as delivered
|
||||
const deliveredNotifications = db.prepare(`
|
||||
SELECT * FROM notifications
|
||||
WHERE recipient = ? AND delivered_at = ?
|
||||
WHERE recipient = ? AND delivered_at = ? AND workspace_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`).all(agent, now) as Notification[];
|
||||
`).all(agent, now, workspaceId) as Notification[];
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
|
|
@ -295,4 +299,4 @@ export async function POST(request: NextRequest) {
|
|||
logger.error({ err: error }, 'POST /api/notifications error');
|
||||
return NextResponse.json({ error: 'Failed to process notification action' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,9 +32,10 @@ export async function GET(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
const pipelines = db.prepare(
|
||||
'SELECT * FROM workflow_pipelines ORDER BY use_count DESC, updated_at DESC'
|
||||
).all() as Pipeline[]
|
||||
'SELECT * FROM workflow_pipelines WHERE workspace_id = ? ORDER BY use_count DESC, updated_at DESC'
|
||||
).all(workspaceId) as Pipeline[]
|
||||
|
||||
// Enrich steps with template names
|
||||
const templates = db.prepare('SELECT id, name FROM workflow_templates').all() as Array<{ id: number; name: string }>
|
||||
|
|
@ -46,8 +47,8 @@ export async function GET(request: NextRequest) {
|
|||
SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as completed,
|
||||
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed,
|
||||
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as running
|
||||
FROM pipeline_runs GROUP BY pipeline_id
|
||||
`).all() as Array<{ pipeline_id: number; total: number; completed: number; failed: number; running: number }>
|
||||
FROM pipeline_runs WHERE workspace_id = ? GROUP BY pipeline_id
|
||||
`).all(workspaceId) as Array<{ pipeline_id: number; total: number; completed: number; failed: number; running: number }>
|
||||
const runMap = new Map(runCounts.map(r => [r.pipeline_id, r]))
|
||||
|
||||
const parsed = pipelines.map(p => {
|
||||
|
|
@ -82,6 +83,7 @@ export async function POST(request: NextRequest) {
|
|||
const { name, description, steps } = result.data
|
||||
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
|
||||
// Validate template IDs exist
|
||||
const templateIds = steps.map((s: PipelineStep) => s.template_id)
|
||||
|
|
@ -98,13 +100,23 @@ export async function POST(request: NextRequest) {
|
|||
}))
|
||||
|
||||
const insertResult = db.prepare(`
|
||||
INSERT INTO workflow_pipelines (name, description, steps, created_by)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(name, description || null, JSON.stringify(cleanSteps), auth.user?.username || 'system')
|
||||
INSERT INTO workflow_pipelines (name, description, steps, created_by, workspace_id)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(name, description || null, JSON.stringify(cleanSteps), auth.user?.username || 'system', workspaceId)
|
||||
|
||||
db_helpers.logActivity('pipeline_created', 'pipeline', Number(insertResult.lastInsertRowid), auth.user?.username || 'system', `Created pipeline: ${name}`)
|
||||
db_helpers.logActivity(
|
||||
'pipeline_created',
|
||||
'pipeline',
|
||||
Number(insertResult.lastInsertRowid),
|
||||
auth.user?.username || 'system',
|
||||
`Created pipeline: ${name}`,
|
||||
undefined,
|
||||
workspaceId
|
||||
)
|
||||
|
||||
const pipeline = db.prepare('SELECT * FROM workflow_pipelines WHERE id = ?').get(insertResult.lastInsertRowid) as Pipeline
|
||||
const pipeline = db
|
||||
.prepare('SELECT * FROM workflow_pipelines WHERE id = ? AND workspace_id = ?')
|
||||
.get(insertResult.lastInsertRowid, workspaceId) as Pipeline
|
||||
return NextResponse.json({ pipeline: { ...pipeline, steps: JSON.parse(pipeline.steps) } }, { status: 201 })
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'POST /api/pipelines error')
|
||||
|
|
@ -121,12 +133,15 @@ export async function PUT(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
const body = await request.json()
|
||||
const { id, ...updates } = body
|
||||
|
||||
if (!id) return NextResponse.json({ error: 'Pipeline ID required' }, { status: 400 })
|
||||
|
||||
const existing = db.prepare('SELECT * FROM workflow_pipelines WHERE id = ?').get(id) as Pipeline
|
||||
const existing = db
|
||||
.prepare('SELECT * FROM workflow_pipelines WHERE id = ? AND workspace_id = ?')
|
||||
.get(id, workspaceId) as Pipeline
|
||||
if (!existing) return NextResponse.json({ error: 'Pipeline not found' }, { status: 404 })
|
||||
|
||||
const fields: string[] = []
|
||||
|
|
@ -147,11 +162,13 @@ export async function PUT(request: NextRequest) {
|
|||
|
||||
fields.push('updated_at = ?')
|
||||
params.push(Math.floor(Date.now() / 1000))
|
||||
params.push(id)
|
||||
params.push(id, workspaceId)
|
||||
|
||||
db.prepare(`UPDATE workflow_pipelines SET ${fields.join(', ')} WHERE id = ?`).run(...params)
|
||||
db.prepare(`UPDATE workflow_pipelines SET ${fields.join(', ')} WHERE id = ? AND workspace_id = ?`).run(...params)
|
||||
|
||||
const updated = db.prepare('SELECT * FROM workflow_pipelines WHERE id = ?').get(id) as Pipeline
|
||||
const updated = db
|
||||
.prepare('SELECT * FROM workflow_pipelines WHERE id = ? AND workspace_id = ?')
|
||||
.get(id, workspaceId) as Pipeline
|
||||
return NextResponse.json({ pipeline: { ...updated, steps: JSON.parse(updated.steps) } })
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'PUT /api/pipelines error')
|
||||
|
|
@ -168,12 +185,13 @@ export async function DELETE(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
let body: any
|
||||
try { body = await request.json() } catch { return NextResponse.json({ error: 'Request body required' }, { status: 400 }) }
|
||||
const id = body.id
|
||||
if (!id) return NextResponse.json({ error: 'Pipeline ID required' }, { status: 400 })
|
||||
|
||||
db.prepare('DELETE FROM workflow_pipelines WHERE id = ?').run(parseInt(id))
|
||||
db.prepare('DELETE FROM workflow_pipelines WHERE id = ? AND workspace_id = ?').run(parseInt(id), workspaceId)
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error({ err: error }, 'DELETE /api/pipelines error')
|
||||
|
|
|
|||
|
|
@ -42,21 +42,24 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const db = getDatabase()
|
||||
const { searchParams } = new URL(request.url)
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
const pipelineId = searchParams.get('pipeline_id')
|
||||
const runId = searchParams.get('id')
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '20'), 200)
|
||||
|
||||
if (runId) {
|
||||
const run = db.prepare('SELECT * FROM pipeline_runs WHERE id = ?').get(parseInt(runId)) as PipelineRun | undefined
|
||||
const run = db
|
||||
.prepare('SELECT * FROM pipeline_runs WHERE id = ? AND workspace_id = ?')
|
||||
.get(parseInt(runId), workspaceId) as PipelineRun | undefined
|
||||
if (!run) return NextResponse.json({ error: 'Run not found' }, { status: 404 })
|
||||
return NextResponse.json({ run: { ...run, steps_snapshot: JSON.parse(run.steps_snapshot) } })
|
||||
}
|
||||
|
||||
let query = 'SELECT * FROM pipeline_runs'
|
||||
const params: any[] = []
|
||||
let query = 'SELECT * FROM pipeline_runs WHERE workspace_id = ?'
|
||||
const params: any[] = [workspaceId]
|
||||
|
||||
if (pipelineId) {
|
||||
query += ' WHERE pipeline_id = ?'
|
||||
query += ' AND pipeline_id = ?'
|
||||
params.push(parseInt(pipelineId))
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +71,7 @@ export async function GET(request: NextRequest) {
|
|||
// Enrich with pipeline names
|
||||
const pipelineIds = [...new Set(runs.map(r => r.pipeline_id))]
|
||||
const pipelines = pipelineIds.length > 0
|
||||
? db.prepare(`SELECT id, name FROM workflow_pipelines WHERE id IN (${pipelineIds.map(() => '?').join(',')})`).all(...pipelineIds) as Array<{ id: number; name: string }>
|
||||
? db.prepare(`SELECT id, name FROM workflow_pipelines WHERE workspace_id = ? AND id IN (${pipelineIds.map(() => '?').join(',')})`).all(workspaceId, ...pipelineIds) as Array<{ id: number; name: string }>
|
||||
: []
|
||||
const nameMap = new Map(pipelines.map(p => [p.id, p.name]))
|
||||
|
||||
|
|
@ -94,15 +97,16 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
const body = await request.json()
|
||||
const { action, pipeline_id, run_id } = body
|
||||
|
||||
if (action === 'start') {
|
||||
return startPipeline(db, pipeline_id, auth.user?.username || 'system')
|
||||
return startPipeline(db, pipeline_id, auth.user?.username || 'system', workspaceId)
|
||||
} else if (action === 'advance') {
|
||||
return advanceRun(db, run_id, body.success ?? true, body.error)
|
||||
return advanceRun(db, run_id, body.success ?? true, body.error, workspaceId)
|
||||
} else if (action === 'cancel') {
|
||||
return cancelRun(db, run_id)
|
||||
return cancelRun(db, run_id, workspaceId)
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: 'Invalid action. Use: start, advance, cancel' }, { status: 400 })
|
||||
|
|
@ -119,7 +123,8 @@ async function spawnStep(
|
|||
template: { name: string; model: string; task_prompt: string; timeout_seconds: number },
|
||||
steps: RunStepState[],
|
||||
stepIdx: number,
|
||||
runId: number
|
||||
runId: number,
|
||||
workspaceId: number
|
||||
): Promise<{ success: boolean; stdout?: string; error?: string }> {
|
||||
try {
|
||||
const { runOpenClaw } = await import('@/lib/command')
|
||||
|
|
@ -133,20 +138,20 @@ async function spawnStep(
|
|||
|
||||
const spawnId = `pipeline-${runId}-step-${stepIdx}-${Date.now()}`
|
||||
steps[stepIdx].spawn_id = spawnId
|
||||
db.prepare('UPDATE pipeline_runs SET steps_snapshot = ? WHERE id = ?').run(JSON.stringify(steps), runId)
|
||||
db.prepare('UPDATE pipeline_runs SET steps_snapshot = ? WHERE id = ? AND workspace_id = ?').run(JSON.stringify(steps), runId, workspaceId)
|
||||
|
||||
return { success: true, stdout: stdout.trim() }
|
||||
} catch (err: any) {
|
||||
// Spawn failed - record error but keep pipeline running for manual advance
|
||||
steps[stepIdx].error = err.message
|
||||
db.prepare('UPDATE pipeline_runs SET steps_snapshot = ? WHERE id = ?').run(JSON.stringify(steps), runId)
|
||||
db.prepare('UPDATE pipeline_runs SET steps_snapshot = ? WHERE id = ? AND workspace_id = ?').run(JSON.stringify(steps), runId, workspaceId)
|
||||
|
||||
return { success: false, error: err.message }
|
||||
}
|
||||
}
|
||||
|
||||
async function startPipeline(db: ReturnType<typeof getDatabase>, pipelineId: number, triggeredBy: string) {
|
||||
const pipeline = db.prepare('SELECT * FROM workflow_pipelines WHERE id = ?').get(pipelineId) as any
|
||||
async function startPipeline(db: ReturnType<typeof getDatabase>, pipelineId: number, triggeredBy: string, workspaceId: number) {
|
||||
const pipeline = db.prepare('SELECT * FROM workflow_pipelines WHERE id = ? AND workspace_id = ?').get(pipelineId, workspaceId) as any
|
||||
if (!pipeline) return NextResponse.json({ error: 'Pipeline not found' }, { status: 404 })
|
||||
|
||||
const steps: PipelineStep[] = JSON.parse(pipeline.steps || '[]')
|
||||
|
|
@ -174,25 +179,25 @@ async function startPipeline(db: ReturnType<typeof getDatabase>, pipelineId: num
|
|||
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const result = db.prepare(`
|
||||
INSERT INTO pipeline_runs (pipeline_id, status, current_step, steps_snapshot, started_at, triggered_by)
|
||||
VALUES (?, 'running', 0, ?, ?, ?)
|
||||
`).run(pipelineId, JSON.stringify(stepsSnapshot), now, triggeredBy)
|
||||
INSERT INTO pipeline_runs (pipeline_id, status, current_step, steps_snapshot, started_at, triggered_by, workspace_id)
|
||||
VALUES (?, 'running', 0, ?, ?, ?, ?)
|
||||
`).run(pipelineId, JSON.stringify(stepsSnapshot), now, triggeredBy, workspaceId)
|
||||
|
||||
const runId = Number(result.lastInsertRowid)
|
||||
|
||||
// Update pipeline usage
|
||||
db.prepare(`
|
||||
UPDATE workflow_pipelines SET use_count = use_count + 1, last_used_at = ?, updated_at = ? WHERE id = ?
|
||||
`).run(now, now, pipelineId)
|
||||
UPDATE workflow_pipelines SET use_count = use_count + 1, last_used_at = ?, updated_at = ? WHERE id = ? AND workspace_id = ?
|
||||
`).run(now, now, pipelineId, workspaceId)
|
||||
|
||||
// Spawn first step
|
||||
const firstTemplate = templateMap.get(steps[0].template_id)
|
||||
let spawnResult: any = null
|
||||
if (firstTemplate) {
|
||||
spawnResult = await spawnStep(db, pipeline.name, firstTemplate, stepsSnapshot, 0, runId)
|
||||
spawnResult = await spawnStep(db, pipeline.name, firstTemplate, stepsSnapshot, 0, runId, workspaceId)
|
||||
}
|
||||
|
||||
db_helpers.logActivity('pipeline_started', 'pipeline', pipelineId, triggeredBy, `Started pipeline: ${pipeline.name}`, { run_id: runId })
|
||||
db_helpers.logActivity('pipeline_started', 'pipeline', pipelineId, triggeredBy, `Started pipeline: ${pipeline.name}`, { run_id: runId }, workspaceId)
|
||||
|
||||
eventBus.broadcast('activity.created', {
|
||||
type: 'pipeline_started',
|
||||
|
|
@ -214,10 +219,10 @@ async function startPipeline(db: ReturnType<typeof getDatabase>, pipelineId: num
|
|||
}, { status: 201 })
|
||||
}
|
||||
|
||||
async function advanceRun(db: ReturnType<typeof getDatabase>, runId: number, success: boolean, errorMsg?: string) {
|
||||
async function advanceRun(db: ReturnType<typeof getDatabase>, runId: number, success: boolean, errorMsg: string | undefined, workspaceId: number) {
|
||||
if (!runId) return NextResponse.json({ error: 'run_id required' }, { status: 400 })
|
||||
|
||||
const run = db.prepare('SELECT * FROM pipeline_runs WHERE id = ?').get(runId) as PipelineRun | undefined
|
||||
const run = db.prepare('SELECT * FROM pipeline_runs WHERE id = ? AND workspace_id = ?').get(runId, workspaceId) as PipelineRun | undefined
|
||||
if (!run) return NextResponse.json({ error: 'Run not found' }, { status: 404 })
|
||||
if (run.status !== 'running') return NextResponse.json({ error: `Run is ${run.status}, not running` }, { status: 400 })
|
||||
|
||||
|
|
@ -237,16 +242,16 @@ async function advanceRun(db: ReturnType<typeof getDatabase>, runId: number, suc
|
|||
if (!success && onFailure === 'stop') {
|
||||
// Mark remaining steps as skipped
|
||||
for (let i = nextIdx; i < steps.length; i++) steps[i].status = 'skipped'
|
||||
db.prepare('UPDATE pipeline_runs SET status = ?, current_step = ?, steps_snapshot = ?, completed_at = ? WHERE id = ?')
|
||||
.run('failed', currentIdx, JSON.stringify(steps), now, runId)
|
||||
db.prepare('UPDATE pipeline_runs SET status = ?, current_step = ?, steps_snapshot = ?, completed_at = ? WHERE id = ? AND workspace_id = ?')
|
||||
.run('failed', currentIdx, JSON.stringify(steps), now, runId, workspaceId)
|
||||
return NextResponse.json({ run: { id: runId, status: 'failed', steps_snapshot: steps } })
|
||||
}
|
||||
|
||||
if (nextIdx >= steps.length) {
|
||||
// Pipeline complete
|
||||
const finalStatus = steps.some(s => s.status === 'failed') ? 'completed' : 'completed'
|
||||
db.prepare('UPDATE pipeline_runs SET status = ?, current_step = ?, steps_snapshot = ?, completed_at = ? WHERE id = ?')
|
||||
.run(finalStatus, currentIdx, JSON.stringify(steps), now, runId)
|
||||
db.prepare('UPDATE pipeline_runs SET status = ?, current_step = ?, steps_snapshot = ?, completed_at = ? WHERE id = ? AND workspace_id = ?')
|
||||
.run(finalStatus, currentIdx, JSON.stringify(steps), now, runId, workspaceId)
|
||||
|
||||
eventBus.broadcast('activity.created', {
|
||||
type: 'pipeline_completed',
|
||||
|
|
@ -267,22 +272,22 @@ async function advanceRun(db: ReturnType<typeof getDatabase>, runId: number, suc
|
|||
|
||||
let spawnResult: any = null
|
||||
if (template) {
|
||||
const pipeline = db.prepare('SELECT name FROM workflow_pipelines WHERE id = ?').get(run.pipeline_id) as any
|
||||
spawnResult = await spawnStep(db, pipeline?.name || '?', template, steps, nextIdx, runId)
|
||||
const pipeline = db.prepare('SELECT name FROM workflow_pipelines WHERE id = ? AND workspace_id = ?').get(run.pipeline_id, workspaceId) as any
|
||||
spawnResult = await spawnStep(db, pipeline?.name || '?', template, steps, nextIdx, runId, workspaceId)
|
||||
}
|
||||
|
||||
db.prepare('UPDATE pipeline_runs SET current_step = ?, steps_snapshot = ? WHERE id = ?')
|
||||
.run(nextIdx, JSON.stringify(steps), runId)
|
||||
db.prepare('UPDATE pipeline_runs SET current_step = ?, steps_snapshot = ? WHERE id = ? AND workspace_id = ?')
|
||||
.run(nextIdx, JSON.stringify(steps), runId, workspaceId)
|
||||
|
||||
return NextResponse.json({
|
||||
run: { id: runId, status: 'running', current_step: nextIdx, steps_snapshot: steps, spawn: spawnResult }
|
||||
})
|
||||
}
|
||||
|
||||
function cancelRun(db: ReturnType<typeof getDatabase>, runId: number) {
|
||||
function cancelRun(db: ReturnType<typeof getDatabase>, runId: number, workspaceId: number) {
|
||||
if (!runId) return NextResponse.json({ error: 'run_id required' }, { status: 400 })
|
||||
|
||||
const run = db.prepare('SELECT * FROM pipeline_runs WHERE id = ?').get(runId) as PipelineRun | undefined
|
||||
const run = db.prepare('SELECT * FROM pipeline_runs WHERE id = ? AND workspace_id = ?').get(runId, workspaceId) as PipelineRun | undefined
|
||||
if (!run) return NextResponse.json({ error: 'Run not found' }, { status: 404 })
|
||||
if (run.status !== 'running' && run.status !== 'pending') {
|
||||
return NextResponse.json({ error: `Run is ${run.status}, cannot cancel` }, { status: 400 })
|
||||
|
|
@ -298,8 +303,8 @@ function cancelRun(db: ReturnType<typeof getDatabase>, runId: number) {
|
|||
}
|
||||
}
|
||||
|
||||
db.prepare('UPDATE pipeline_runs SET status = ?, steps_snapshot = ?, completed_at = ? WHERE id = ?')
|
||||
.run('cancelled', JSON.stringify(steps), now, runId)
|
||||
db.prepare('UPDATE pipeline_runs SET status = ?, steps_snapshot = ?, completed_at = ? WHERE id = ? AND workspace_id = ?')
|
||||
.run('cancelled', JSON.stringify(steps), now, runId, workspaceId)
|
||||
|
||||
return NextResponse.json({ run: { id: runId, status: 'cancelled', steps_snapshot: steps } })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const db = getDatabase()
|
||||
const { searchParams } = new URL(request.url)
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const taskIdsParam = searchParams.get('taskIds')
|
||||
const taskId = parseInt(searchParams.get('taskId') || '')
|
||||
|
||||
|
|
@ -29,9 +30,9 @@ export async function GET(request: NextRequest) {
|
|||
const placeholders = ids.map(() => '?').join(',')
|
||||
const rows = db.prepare(`
|
||||
SELECT * FROM quality_reviews
|
||||
WHERE task_id IN (${placeholders})
|
||||
WHERE task_id IN (${placeholders}) AND workspace_id = ?
|
||||
ORDER BY task_id ASC, created_at DESC
|
||||
`).all(...ids) as Array<{ task_id: number; reviewer?: string; status?: string; created_at?: number }>
|
||||
`).all(...ids, workspaceId) as Array<{ task_id: number; reviewer?: string; status?: string; created_at?: number }>
|
||||
|
||||
const byTask: Record<number, { status?: string; reviewer?: string; created_at?: number } | null> = {}
|
||||
for (const id of ids) {
|
||||
|
|
@ -54,10 +55,10 @@ export async function GET(request: NextRequest) {
|
|||
|
||||
const reviews = db.prepare(`
|
||||
SELECT * FROM quality_reviews
|
||||
WHERE task_id = ?
|
||||
WHERE task_id = ? AND workspace_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 10
|
||||
`).all(taskId)
|
||||
`).all(taskId, workspaceId)
|
||||
|
||||
return NextResponse.json({ reviews })
|
||||
} catch (error) {
|
||||
|
|
@ -79,16 +80,19 @@ export async function POST(request: NextRequest) {
|
|||
const { taskId, reviewer, status, notes } = validated.data
|
||||
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
const task = db.prepare('SELECT id, title FROM tasks WHERE id = ?').get(taskId) as any
|
||||
const task = db
|
||||
.prepare('SELECT id, title FROM tasks WHERE id = ? AND workspace_id = ?')
|
||||
.get(taskId, workspaceId) as any
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const result = db.prepare(`
|
||||
INSERT INTO quality_reviews (task_id, reviewer, status, notes)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(taskId, reviewer, status, notes)
|
||||
INSERT INTO quality_reviews (task_id, reviewer, status, notes, workspace_id)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(taskId, reviewer, status, notes, workspaceId)
|
||||
|
||||
db_helpers.logActivity(
|
||||
'quality_review',
|
||||
|
|
@ -96,13 +100,14 @@ export async function POST(request: NextRequest) {
|
|||
taskId,
|
||||
reviewer,
|
||||
`Quality review ${status} for task: ${task.title}`,
|
||||
{ status, notes }
|
||||
{ status, notes },
|
||||
workspaceId
|
||||
)
|
||||
|
||||
// Auto-advance task to 'done' when aegis approves
|
||||
if (status === 'approved' && reviewer === 'aegis') {
|
||||
db.prepare('UPDATE tasks SET status = ?, updated_at = unixepoch() WHERE id = ?')
|
||||
.run('done', taskId)
|
||||
db.prepare('UPDATE tasks SET status = ?, updated_at = unixepoch() WHERE id = ? AND workspace_id = ?')
|
||||
.run('done', taskId, workspaceId)
|
||||
eventBus.broadcast('task.status_changed', {
|
||||
id: taskId,
|
||||
status: 'done',
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export async function GET(request: NextRequest) {
|
|||
}
|
||||
|
||||
const db = getDatabase()
|
||||
const workspaceId = auth.user.workspace_id ?? 1
|
||||
const likeQ = `%${query}%`
|
||||
const results: SearchResult[] = []
|
||||
|
||||
|
|
@ -42,9 +43,9 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const tasks = db.prepare(`
|
||||
SELECT id, title, description, status, assigned_to, created_at
|
||||
FROM tasks WHERE title LIKE ? OR description LIKE ? OR assigned_to LIKE ?
|
||||
FROM tasks WHERE workspace_id = ? AND (title LIKE ? OR description LIKE ? OR assigned_to LIKE ?)
|
||||
ORDER BY created_at DESC LIMIT ?
|
||||
`).all(likeQ, likeQ, likeQ, limit) as any[]
|
||||
`).all(workspaceId, likeQ, likeQ, likeQ, limit) as any[]
|
||||
for (const t of tasks) {
|
||||
results.push({
|
||||
type: 'task',
|
||||
|
|
@ -64,9 +65,9 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const agents = db.prepare(`
|
||||
SELECT id, name, role, status, last_activity, created_at
|
||||
FROM agents WHERE name LIKE ? OR role LIKE ? OR last_activity LIKE ?
|
||||
FROM agents WHERE workspace_id = ? AND (name LIKE ? OR role LIKE ? OR last_activity LIKE ?)
|
||||
ORDER BY created_at DESC LIMIT ?
|
||||
`).all(likeQ, likeQ, likeQ, limit) as any[]
|
||||
`).all(workspaceId, likeQ, likeQ, likeQ, limit) as any[]
|
||||
for (const a of agents) {
|
||||
results.push({
|
||||
type: 'agent',
|
||||
|
|
@ -86,9 +87,9 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const activities = db.prepare(`
|
||||
SELECT id, type, actor, description, created_at
|
||||
FROM activities WHERE description LIKE ? OR actor LIKE ?
|
||||
FROM activities WHERE workspace_id = ? AND (description LIKE ? OR actor LIKE ?)
|
||||
ORDER BY created_at DESC LIMIT ?
|
||||
`).all(likeQ, likeQ, limit) as any[]
|
||||
`).all(workspaceId, likeQ, likeQ, limit) as any[]
|
||||
for (const a of activities) {
|
||||
results.push({
|
||||
type: 'activity',
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export async function POST(request: NextRequest) {
|
|||
try {
|
||||
const db = getDatabase();
|
||||
const body = await request.json();
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
// Parse parameters
|
||||
const targetDate = body.date || new Date().toISOString().split('T')[0]; // YYYY-MM-DD format
|
||||
|
|
@ -24,12 +25,12 @@ export async function POST(request: NextRequest) {
|
|||
const endOfDay = Math.floor(new Date(`${targetDate}T23:59:59Z`).getTime() / 1000);
|
||||
|
||||
// Get all active agents or filter by specific agents
|
||||
let agentQuery = 'SELECT * FROM agents';
|
||||
const agentParams: any[] = [];
|
||||
let agentQuery = 'SELECT * FROM agents WHERE workspace_id = ?';
|
||||
const agentParams: any[] = [workspaceId];
|
||||
|
||||
if (specificAgents && Array.isArray(specificAgents) && specificAgents.length > 0) {
|
||||
const placeholders = specificAgents.map(() => '?').join(',');
|
||||
agentQuery += ` WHERE name IN (${placeholders})`;
|
||||
agentQuery += ` AND name IN (${placeholders})`;
|
||||
agentParams.push(...specificAgents);
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +43,7 @@ export async function POST(request: NextRequest) {
|
|||
SELECT id, title, status, updated_at
|
||||
FROM tasks
|
||||
WHERE assigned_to = ?
|
||||
AND workspace_id = ?
|
||||
AND status = 'done'
|
||||
AND updated_at BETWEEN ? AND ?
|
||||
ORDER BY updated_at DESC
|
||||
|
|
@ -50,6 +52,7 @@ export async function POST(request: NextRequest) {
|
|||
SELECT id, title, status, created_at, due_date
|
||||
FROM tasks
|
||||
WHERE assigned_to = ?
|
||||
AND workspace_id = ?
|
||||
AND status = 'in_progress'
|
||||
ORDER BY created_at ASC
|
||||
`);
|
||||
|
|
@ -57,6 +60,7 @@ export async function POST(request: NextRequest) {
|
|||
SELECT id, title, status, created_at, due_date, priority
|
||||
FROM tasks
|
||||
WHERE assigned_to = ?
|
||||
AND workspace_id = ?
|
||||
AND status = 'assigned'
|
||||
ORDER BY priority DESC, created_at ASC
|
||||
`);
|
||||
|
|
@ -64,6 +68,7 @@ export async function POST(request: NextRequest) {
|
|||
SELECT id, title, status, updated_at
|
||||
FROM tasks
|
||||
WHERE assigned_to = ?
|
||||
AND workspace_id = ?
|
||||
AND status IN ('review', 'quality_review')
|
||||
ORDER BY updated_at ASC
|
||||
`);
|
||||
|
|
@ -71,6 +76,7 @@ export async function POST(request: NextRequest) {
|
|||
SELECT id, title, status, priority, created_at, metadata
|
||||
FROM tasks
|
||||
WHERE assigned_to = ?
|
||||
AND workspace_id = ?
|
||||
AND (priority = 'urgent' OR metadata LIKE '%blocked%')
|
||||
AND status NOT IN ('done')
|
||||
ORDER BY priority DESC, created_at ASC
|
||||
|
|
@ -79,24 +85,26 @@ export async function POST(request: NextRequest) {
|
|||
SELECT COUNT(*) as count
|
||||
FROM activities
|
||||
WHERE actor = ?
|
||||
AND workspace_id = ?
|
||||
AND created_at BETWEEN ? AND ?
|
||||
`);
|
||||
const commentCountStmt = db.prepare(`
|
||||
SELECT COUNT(*) as count
|
||||
FROM comments
|
||||
WHERE author = ?
|
||||
AND workspace_id = ?
|
||||
AND created_at BETWEEN ? AND ?
|
||||
`);
|
||||
|
||||
// Generate standup data for each agent
|
||||
const standupData = agents.map(agent => {
|
||||
const completedTasks = completedTasksStmt.all(agent.name, startOfDay, endOfDay);
|
||||
const inProgressTasks = inProgressTasksStmt.all(agent.name);
|
||||
const assignedTasks = assignedTasksStmt.all(agent.name);
|
||||
const reviewTasks = reviewTasksStmt.all(agent.name);
|
||||
const blockedTasks = blockedTasksStmt.all(agent.name);
|
||||
const activityCount = activityCountStmt.get(agent.name, startOfDay, endOfDay) as { count: number };
|
||||
const commentsToday = commentCountStmt.get(agent.name, startOfDay, endOfDay) as { count: number };
|
||||
const completedTasks = completedTasksStmt.all(agent.name, workspaceId, startOfDay, endOfDay);
|
||||
const inProgressTasks = inProgressTasksStmt.all(agent.name, workspaceId);
|
||||
const assignedTasks = assignedTasksStmt.all(agent.name, workspaceId);
|
||||
const reviewTasks = reviewTasksStmt.all(agent.name, workspaceId);
|
||||
const blockedTasks = blockedTasksStmt.all(agent.name, workspaceId);
|
||||
const activityCount = activityCountStmt.get(agent.name, workspaceId, startOfDay, endOfDay) as { count: number };
|
||||
const commentsToday = commentCountStmt.get(agent.name, workspaceId, startOfDay, endOfDay) as { count: number };
|
||||
|
||||
return {
|
||||
agent: {
|
||||
|
|
@ -145,10 +153,12 @@ export async function POST(request: NextRequest) {
|
|||
SELECT t.*, a.name as agent_name
|
||||
FROM tasks t
|
||||
LEFT JOIN agents a ON t.assigned_to = a.name
|
||||
AND a.workspace_id = t.workspace_id
|
||||
WHERE t.due_date < ?
|
||||
AND t.workspace_id = ?
|
||||
AND t.status NOT IN ('done')
|
||||
ORDER BY t.due_date ASC
|
||||
`).all(now);
|
||||
`).all(now, workspaceId);
|
||||
|
||||
const standupReport = {
|
||||
date: targetDate,
|
||||
|
|
@ -172,16 +182,16 @@ export async function POST(request: NextRequest) {
|
|||
// Persist standup report
|
||||
const createdAt = Math.floor(Date.now() / 1000);
|
||||
db.prepare(`
|
||||
INSERT OR REPLACE INTO standup_reports (date, report, created_at)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(targetDate, JSON.stringify(standupReport), createdAt);
|
||||
INSERT OR REPLACE INTO standup_reports (date, report, created_at, workspace_id)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(targetDate, JSON.stringify(standupReport), createdAt, workspaceId);
|
||||
|
||||
// Log the standup generation
|
||||
db_helpers.logActivity(
|
||||
'standup_generated',
|
||||
'standup',
|
||||
0, // No specific entity
|
||||
'system',
|
||||
auth.user.username,
|
||||
`Generated daily standup for ${targetDate}`,
|
||||
{
|
||||
date: targetDate,
|
||||
|
|
@ -193,7 +203,8 @@ export async function POST(request: NextRequest) {
|
|||
review: totalReview,
|
||||
blocked: totalBlocked
|
||||
}
|
||||
}
|
||||
},
|
||||
workspaceId
|
||||
);
|
||||
|
||||
return NextResponse.json({ standup: standupReport });
|
||||
|
|
@ -214,6 +225,7 @@ export async function GET(request: NextRequest) {
|
|||
try {
|
||||
const db = getDatabase();
|
||||
const { searchParams } = new URL(request.url);
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
const limit = Math.min(parseInt(searchParams.get('limit') || '10'), 200);
|
||||
const offset = parseInt(searchParams.get('offset') || '0');
|
||||
|
|
@ -221,9 +233,10 @@ export async function GET(request: NextRequest) {
|
|||
const standupRows = db.prepare(`
|
||||
SELECT date, report, created_at
|
||||
FROM standup_reports
|
||||
WHERE workspace_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`).all(limit, offset) as Array<{ date: string; report: string; created_at: number }>;
|
||||
`).all(workspaceId, limit, offset) as Array<{ date: string; report: string; created_at: number }>;
|
||||
|
||||
const standupHistory = standupRows.map((row, index) => {
|
||||
const report = row.report ? JSON.parse(row.report) : {};
|
||||
|
|
@ -236,7 +249,9 @@ export async function GET(request: NextRequest) {
|
|||
};
|
||||
});
|
||||
|
||||
const countRow = db.prepare('SELECT COUNT(*) as total FROM standup_reports').get() as { total: number };
|
||||
const countRow = db
|
||||
.prepare('SELECT COUNT(*) as total FROM standup_reports WHERE workspace_id = ?')
|
||||
.get(workspaceId) as { total: number };
|
||||
|
||||
return NextResponse.json({
|
||||
history: standupHistory,
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ export async function GET(request: NextRequest) {
|
|||
const action = searchParams.get('action') || 'overview'
|
||||
|
||||
if (action === 'overview') {
|
||||
const status = await getSystemStatus()
|
||||
const status = await getSystemStatus(auth.user.workspace_id ?? 1)
|
||||
return NextResponse.json(status)
|
||||
}
|
||||
|
||||
if (action === 'dashboard') {
|
||||
const data = await getDashboardData()
|
||||
const data = await getDashboardData(auth.user.workspace_id ?? 1)
|
||||
return NextResponse.json(data)
|
||||
}
|
||||
|
||||
|
|
@ -59,16 +59,16 @@ export async function GET(request: NextRequest) {
|
|||
* Aggregate all dashboard data in a single request.
|
||||
* Combines system health, DB stats, audit summary, and recent activity.
|
||||
*/
|
||||
async function getDashboardData() {
|
||||
async function getDashboardData(workspaceId: number) {
|
||||
const [system, dbStats] = await Promise.all([
|
||||
getSystemStatus(),
|
||||
getDbStats(),
|
||||
getSystemStatus(workspaceId),
|
||||
getDbStats(workspaceId),
|
||||
])
|
||||
|
||||
return { ...system, db: dbStats }
|
||||
}
|
||||
|
||||
function getDbStats() {
|
||||
function getDbStats(workspaceId: number) {
|
||||
try {
|
||||
const db = getDatabase()
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
|
|
@ -77,8 +77,8 @@ function getDbStats() {
|
|||
|
||||
// Task breakdown
|
||||
const taskStats = db.prepare(`
|
||||
SELECT status, COUNT(*) as count FROM tasks GROUP BY status
|
||||
`).all() as Array<{ status: string; count: number }>
|
||||
SELECT status, COUNT(*) as count FROM tasks WHERE workspace_id = ? GROUP BY status
|
||||
`).all(workspaceId) as Array<{ status: string; count: number }>
|
||||
const tasksByStatus: Record<string, number> = {}
|
||||
let totalTasks = 0
|
||||
for (const row of taskStats) {
|
||||
|
|
@ -88,8 +88,8 @@ function getDbStats() {
|
|||
|
||||
// Agent breakdown
|
||||
const agentStats = db.prepare(`
|
||||
SELECT status, COUNT(*) as count FROM agents GROUP BY status
|
||||
`).all() as Array<{ status: string; count: number }>
|
||||
SELECT status, COUNT(*) as count FROM agents WHERE workspace_id = ? GROUP BY status
|
||||
`).all(workspaceId) as Array<{ status: string; count: number }>
|
||||
const agentsByStatus: Record<string, number> = {}
|
||||
let totalAgents = 0
|
||||
for (const row of agentStats) {
|
||||
|
|
@ -107,10 +107,14 @@ function getDbStats() {
|
|||
).get(day) as any).c
|
||||
|
||||
// Activities (24h)
|
||||
const activityDay = (db.prepare('SELECT COUNT(*) as c FROM activities WHERE created_at > ?').get(day) as any).c
|
||||
const activityDay = (
|
||||
db.prepare('SELECT COUNT(*) as c FROM activities WHERE created_at > ? AND workspace_id = ?').get(day, workspaceId) as any
|
||||
).c
|
||||
|
||||
// Notifications (unread)
|
||||
const unreadNotifs = (db.prepare('SELECT COUNT(*) as c FROM notifications WHERE read_at IS NULL').get() as any).c
|
||||
const unreadNotifs = (
|
||||
db.prepare('SELECT COUNT(*) as c FROM notifications WHERE read_at IS NULL AND workspace_id = ?').get(workspaceId) as any
|
||||
).c
|
||||
|
||||
// Pipeline runs (active + recent)
|
||||
let pipelineActive = 0
|
||||
|
|
@ -179,7 +183,7 @@ function getDbStats() {
|
|||
}
|
||||
}
|
||||
|
||||
async function getSystemStatus() {
|
||||
async function getSystemStatus(workspaceId: number) {
|
||||
const status: any = {
|
||||
timestamp: Date.now(),
|
||||
uptime: 0,
|
||||
|
|
@ -277,14 +281,16 @@ async function getSystemStatus() {
|
|||
// Match by: exact name, lowercase, or normalized (spaces→hyphens)
|
||||
const updateStmt = db.prepare(
|
||||
`UPDATE agents SET status = ?, last_seen = ?, updated_at = ?
|
||||
WHERE LOWER(name) = LOWER(?)
|
||||
OR LOWER(REPLACE(name, ' ', '-')) = LOWER(?)`
|
||||
WHERE workspace_id = ?
|
||||
AND (LOWER(name) = LOWER(?)
|
||||
OR LOWER(REPLACE(name, ' ', '-')) = LOWER(?))`
|
||||
)
|
||||
for (const [agentName, info] of liveStatuses) {
|
||||
updateStmt.run(
|
||||
info.status,
|
||||
Math.floor(info.lastActivity / 1000),
|
||||
now,
|
||||
workspaceId,
|
||||
agentName,
|
||||
agentName
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export async function POST(
|
|||
const resolvedParams = await params
|
||||
const taskId = parseInt(resolvedParams.id)
|
||||
const body = await request.json()
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const author = (body.author || 'system') as string
|
||||
const message = (body.message || '').trim()
|
||||
|
||||
|
|
@ -26,12 +27,14 @@ export async function POST(
|
|||
}
|
||||
|
||||
const db = getDatabase()
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) as any
|
||||
const task = db
|
||||
.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?')
|
||||
.get(taskId, workspaceId) as any
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
const subscribers = new Set(db_helpers.getTaskSubscribers(taskId))
|
||||
const subscribers = new Set(db_helpers.getTaskSubscribers(taskId, workspaceId))
|
||||
subscribers.delete(author)
|
||||
|
||||
if (subscribers.size === 0) {
|
||||
|
|
@ -39,8 +42,8 @@ export async function POST(
|
|||
}
|
||||
|
||||
const agents = db
|
||||
.prepare('SELECT name, session_key FROM agents WHERE name IN (' + Array.from(subscribers).map(() => '?').join(',') + ')')
|
||||
.all(...Array.from(subscribers)) as Array<{ name: string; session_key?: string }>
|
||||
.prepare('SELECT name, session_key FROM agents WHERE workspace_id = ? AND name IN (' + Array.from(subscribers).map(() => '?').join(',') + ')')
|
||||
.all(workspaceId, ...Array.from(subscribers)) as Array<{ name: string; session_key?: string }>
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
agents.map(async (agent) => {
|
||||
|
|
@ -62,7 +65,8 @@ export async function POST(
|
|||
'Task Broadcast',
|
||||
`${author} broadcasted a message on "${task.title}": ${message.substring(0, 100)}${message.length > 100 ? '...' : ''}`,
|
||||
'task',
|
||||
taskId
|
||||
taskId,
|
||||
workspaceId
|
||||
)
|
||||
return 'sent'
|
||||
})
|
||||
|
|
@ -81,7 +85,8 @@ export async function POST(
|
|||
taskId,
|
||||
author,
|
||||
`Broadcasted message to ${sent} subscribers`,
|
||||
{ sent, skipped }
|
||||
{ sent, skipped },
|
||||
workspaceId
|
||||
)
|
||||
|
||||
return NextResponse.json({ sent, skipped })
|
||||
|
|
|
|||
|
|
@ -19,13 +19,16 @@ export async function GET(
|
|||
const db = getDatabase();
|
||||
const resolvedParams = await params;
|
||||
const taskId = parseInt(resolvedParams.id);
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
if (isNaN(taskId)) {
|
||||
return NextResponse.json({ error: 'Invalid task ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify task exists
|
||||
const task = db.prepare('SELECT id FROM tasks WHERE id = ?').get(taskId);
|
||||
const task = db
|
||||
.prepare('SELECT id FROM tasks WHERE id = ? AND workspace_id = ?')
|
||||
.get(taskId, workspaceId);
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
|
@ -33,11 +36,11 @@ export async function GET(
|
|||
// Get comments ordered by creation time
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM comments
|
||||
WHERE task_id = ?
|
||||
WHERE task_id = ? AND workspace_id = ?
|
||||
ORDER BY created_at ASC
|
||||
`);
|
||||
|
||||
const comments = stmt.all(taskId) as Comment[];
|
||||
const comments = stmt.all(taskId, workspaceId) as Comment[];
|
||||
|
||||
// Parse JSON fields and build thread structure
|
||||
const commentsWithParsedData = comments.map(comment => ({
|
||||
|
|
@ -97,6 +100,7 @@ export async function POST(
|
|||
const db = getDatabase();
|
||||
const resolvedParams = await params;
|
||||
const taskId = parseInt(resolvedParams.id);
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
if (isNaN(taskId)) {
|
||||
return NextResponse.json({ error: 'Invalid task ID' }, { status: 400 });
|
||||
|
|
@ -107,14 +111,18 @@ export async function POST(
|
|||
const { content, author = 'system', parent_id } = result.data;
|
||||
|
||||
// Verify task exists
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) as any;
|
||||
const task = db
|
||||
.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?')
|
||||
.get(taskId, workspaceId) as any;
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Verify parent comment exists if specified
|
||||
if (parent_id) {
|
||||
const parentComment = db.prepare('SELECT id FROM comments WHERE id = ? AND task_id = ?').get(parent_id, taskId);
|
||||
const parentComment = db
|
||||
.prepare('SELECT id FROM comments WHERE id = ? AND task_id = ? AND workspace_id = ?')
|
||||
.get(parent_id, taskId, workspaceId);
|
||||
if (!parentComment) {
|
||||
return NextResponse.json({ error: 'Parent comment not found' }, { status: 404 });
|
||||
}
|
||||
|
|
@ -127,8 +135,8 @@ export async function POST(
|
|||
|
||||
// Insert comment
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO comments (task_id, author, content, created_at, parent_id, mentions)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO comments (task_id, author, content, created_at, parent_id, mentions, workspace_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const insertResult = stmt.run(
|
||||
|
|
@ -137,7 +145,8 @@ export async function POST(
|
|||
content,
|
||||
now,
|
||||
parent_id || null,
|
||||
mentions.length > 0 ? JSON.stringify(mentions) : null
|
||||
mentions.length > 0 ? JSON.stringify(mentions) : null,
|
||||
workspaceId
|
||||
);
|
||||
|
||||
const commentId = insertResult.lastInsertRowid as number;
|
||||
|
|
@ -159,21 +168,22 @@ export async function POST(
|
|||
parent_id,
|
||||
mentions,
|
||||
content_preview: content.substring(0, 100)
|
||||
}
|
||||
},
|
||||
workspaceId
|
||||
);
|
||||
|
||||
// Ensure subscriptions for author, mentions, and assignee
|
||||
db_helpers.ensureTaskSubscription(taskId, author);
|
||||
db_helpers.ensureTaskSubscription(taskId, author, workspaceId);
|
||||
const uniqueMentions = Array.from(new Set(mentions));
|
||||
uniqueMentions.forEach((mentionedAgent) => {
|
||||
db_helpers.ensureTaskSubscription(taskId, mentionedAgent);
|
||||
db_helpers.ensureTaskSubscription(taskId, mentionedAgent, workspaceId);
|
||||
});
|
||||
if (task.assigned_to) {
|
||||
db_helpers.ensureTaskSubscription(taskId, task.assigned_to);
|
||||
db_helpers.ensureTaskSubscription(taskId, task.assigned_to, workspaceId);
|
||||
}
|
||||
|
||||
// Notify subscribers
|
||||
const subscribers = new Set(db_helpers.getTaskSubscribers(taskId));
|
||||
const subscribers = new Set(db_helpers.getTaskSubscribers(taskId, workspaceId));
|
||||
subscribers.delete(author);
|
||||
const mentionSet = new Set(uniqueMentions);
|
||||
|
||||
|
|
@ -187,12 +197,15 @@ export async function POST(
|
|||
? `${author} mentioned you in a comment on "${task.title}": ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`
|
||||
: `${author} commented on "${task.title}": ${content.substring(0, 100)}${content.length > 100 ? '...' : ''}`,
|
||||
'comment',
|
||||
commentId
|
||||
commentId,
|
||||
workspaceId
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch the created comment
|
||||
const createdComment = db.prepare('SELECT * FROM comments WHERE id = ?').get(commentId) as Comment;
|
||||
const createdComment = db
|
||||
.prepare('SELECT * FROM comments WHERE id = ? AND workspace_id = ?')
|
||||
.get(commentId, workspaceId) as Comment;
|
||||
|
||||
return NextResponse.json({
|
||||
comment: {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { getDatabase, Task, db_helpers } from '@/lib/db';
|
||||
import { eventBus } from '@/lib/event-bus';
|
||||
import { getUserFromRequest, requireRole } from '@/lib/auth';
|
||||
import { requireRole } from '@/lib/auth';
|
||||
import { mutationLimiter } from '@/lib/rate-limit';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { validateBody, updateTaskSchema } from '@/lib/validation';
|
||||
|
||||
function hasAegisApproval(db: ReturnType<typeof getDatabase>, taskId: number): boolean {
|
||||
function hasAegisApproval(
|
||||
db: ReturnType<typeof getDatabase>,
|
||||
taskId: number,
|
||||
workspaceId: number
|
||||
): boolean {
|
||||
const review = db.prepare(`
|
||||
SELECT status FROM quality_reviews
|
||||
WHERE task_id = ? AND reviewer = 'aegis'
|
||||
WHERE task_id = ? AND reviewer = 'aegis' AND workspace_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`).get(taskId) as { status?: string } | undefined
|
||||
`).get(taskId, workspaceId) as { status?: string } | undefined
|
||||
return review?.status === 'approved'
|
||||
}
|
||||
|
||||
|
|
@ -30,13 +34,14 @@ export async function GET(
|
|||
const db = getDatabase();
|
||||
const resolvedParams = await params;
|
||||
const taskId = parseInt(resolvedParams.id);
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
if (isNaN(taskId)) {
|
||||
return NextResponse.json({ error: 'Invalid task ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const stmt = db.prepare('SELECT * FROM tasks WHERE id = ?');
|
||||
const task = stmt.get(taskId) as Task;
|
||||
const stmt = db.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?');
|
||||
const task = stmt.get(taskId, workspaceId) as Task;
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
||||
|
|
@ -73,6 +78,7 @@ export async function PUT(
|
|||
const db = getDatabase();
|
||||
const resolvedParams = await params;
|
||||
const taskId = parseInt(resolvedParams.id);
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
const validated = await validateBody(request, updateTaskSchema);
|
||||
if ('error' in validated) return validated.error;
|
||||
const body = validated.data;
|
||||
|
|
@ -82,7 +88,9 @@ export async function PUT(
|
|||
}
|
||||
|
||||
// Get current task for comparison
|
||||
const currentTask = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) as Task;
|
||||
const currentTask = db
|
||||
.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?')
|
||||
.get(taskId, workspaceId) as Task;
|
||||
|
||||
if (!currentTask) {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
||||
|
|
@ -116,7 +124,7 @@ export async function PUT(
|
|||
updateParams.push(description);
|
||||
}
|
||||
if (status !== undefined) {
|
||||
if (status === 'done' && !hasAegisApproval(db, taskId)) {
|
||||
if (status === 'done' && !hasAegisApproval(db, taskId, workspaceId)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Aegis approval is required to move task to done.' },
|
||||
{ status: 403 }
|
||||
|
|
@ -156,7 +164,7 @@ export async function PUT(
|
|||
|
||||
fieldsToUpdate.push('updated_at = ?');
|
||||
updateParams.push(now);
|
||||
updateParams.push(taskId);
|
||||
updateParams.push(taskId, workspaceId);
|
||||
|
||||
if (fieldsToUpdate.length === 1) { // Only updated_at
|
||||
return NextResponse.json({ error: 'No fields to update' }, { status: 400 });
|
||||
|
|
@ -165,7 +173,7 @@ export async function PUT(
|
|||
const stmt = db.prepare(`
|
||||
UPDATE tasks
|
||||
SET ${fieldsToUpdate.join(', ')}
|
||||
WHERE id = ?
|
||||
WHERE id = ? AND workspace_id = ?
|
||||
`);
|
||||
|
||||
stmt.run(...updateParams);
|
||||
|
|
@ -184,7 +192,8 @@ export async function PUT(
|
|||
'Task Status Updated',
|
||||
`Task "${currentTask.title}" status changed to ${status}`,
|
||||
'task',
|
||||
taskId
|
||||
taskId,
|
||||
workspaceId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -194,14 +203,15 @@ export async function PUT(
|
|||
|
||||
// Create notification for new assignee
|
||||
if (assigned_to) {
|
||||
db_helpers.ensureTaskSubscription(taskId, assigned_to);
|
||||
db_helpers.ensureTaskSubscription(taskId, assigned_to, workspaceId);
|
||||
db_helpers.createNotification(
|
||||
assigned_to,
|
||||
'assignment',
|
||||
'Task Assigned',
|
||||
`You have been assigned to task: ${currentTask.title}`,
|
||||
'task',
|
||||
taskId
|
||||
taskId,
|
||||
workspaceId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -220,7 +230,7 @@ export async function PUT(
|
|||
'task_updated',
|
||||
'task',
|
||||
taskId,
|
||||
getUserFromRequest(request)?.username || 'system',
|
||||
auth.user.username,
|
||||
`Task updated: ${changes.join(', ')}`,
|
||||
{
|
||||
changes: changes,
|
||||
|
|
@ -231,12 +241,15 @@ export async function PUT(
|
|||
assigned_to: currentTask.assigned_to
|
||||
},
|
||||
newValues: { title, status, priority, assigned_to }
|
||||
}
|
||||
},
|
||||
workspaceId
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch updated task
|
||||
const updatedTask = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) as Task;
|
||||
const updatedTask = db
|
||||
.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?')
|
||||
.get(taskId, workspaceId) as Task;
|
||||
const parsedTask = {
|
||||
...updatedTask,
|
||||
tags: updatedTask.tags ? JSON.parse(updatedTask.tags) : [],
|
||||
|
|
@ -270,34 +283,38 @@ export async function DELETE(
|
|||
const db = getDatabase();
|
||||
const resolvedParams = await params;
|
||||
const taskId = parseInt(resolvedParams.id);
|
||||
const workspaceId = auth.user.workspace_id ?? 1;
|
||||
|
||||
if (isNaN(taskId)) {
|
||||
return NextResponse.json({ error: 'Invalid task ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Get task before deletion for logging
|
||||
const task = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) as Task;
|
||||
const task = db
|
||||
.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?')
|
||||
.get(taskId, workspaceId) as Task;
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Delete task (cascades will handle comments)
|
||||
const stmt = db.prepare('DELETE FROM tasks WHERE id = ?');
|
||||
stmt.run(taskId);
|
||||
const stmt = db.prepare('DELETE FROM tasks WHERE id = ? AND workspace_id = ?');
|
||||
stmt.run(taskId, workspaceId);
|
||||
|
||||
// Log deletion
|
||||
db_helpers.logActivity(
|
||||
'task_deleted',
|
||||
'task',
|
||||
taskId,
|
||||
getUserFromRequest(request)?.username || 'system',
|
||||
auth.user.username,
|
||||
`Deleted task: ${task.title}`,
|
||||
{
|
||||
title: task.title,
|
||||
status: task.status,
|
||||
assigned_to: task.assigned_to
|
||||
}
|
||||
},
|
||||
workspaceId
|
||||
);
|
||||
|
||||
// Broadcast to SSE clients
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import { mutationLimiter } from '@/lib/rate-limit';
|
|||
import { logger } from '@/lib/logger';
|
||||
import { validateBody, createTaskSchema, bulkUpdateTaskStatusSchema } from '@/lib/validation';
|
||||
|
||||
function hasAegisApproval(db: ReturnType<typeof getDatabase>, taskId: number): boolean {
|
||||
function hasAegisApproval(db: ReturnType<typeof getDatabase>, taskId: number, workspaceId: number): boolean {
|
||||
const review = db.prepare(`
|
||||
SELECT status FROM quality_reviews
|
||||
WHERE task_id = ? AND reviewer = 'aegis'
|
||||
WHERE task_id = ? AND reviewer = 'aegis' AND workspace_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`).get(taskId) as { status?: string } | undefined
|
||||
`).get(taskId, workspaceId) as { status?: string } | undefined
|
||||
return review?.status === 'approved'
|
||||
}
|
||||
|
||||
|
|
@ -26,6 +26,7 @@ export async function GET(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const workspaceId = auth.user.workspace_id;
|
||||
const { searchParams } = new URL(request.url);
|
||||
|
||||
// Parse query parameters
|
||||
|
|
@ -36,8 +37,8 @@ export async function GET(request: NextRequest) {
|
|||
const offset = parseInt(searchParams.get('offset') || '0');
|
||||
|
||||
// Build dynamic query
|
||||
let query = 'SELECT * FROM tasks WHERE 1=1';
|
||||
const params: any[] = [];
|
||||
let query = 'SELECT * FROM tasks WHERE workspace_id = ?';
|
||||
const params: any[] = [workspaceId];
|
||||
|
||||
if (status) {
|
||||
query += ' AND status = ?';
|
||||
|
|
@ -68,8 +69,8 @@ export async function GET(request: NextRequest) {
|
|||
}));
|
||||
|
||||
// Get total count for pagination
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM tasks WHERE 1=1';
|
||||
const countParams: any[] = [];
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM tasks WHERE workspace_id = ?';
|
||||
const countParams: any[] = [workspaceId];
|
||||
if (status) {
|
||||
countQuery += ' AND status = ?';
|
||||
countParams.push(status);
|
||||
|
|
@ -103,6 +104,7 @@ export async function POST(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const workspaceId = auth.user.workspace_id;
|
||||
const validated = await validateBody(request, createTaskSchema);
|
||||
if ('error' in validated) return validated.error;
|
||||
const body = validated.data;
|
||||
|
|
@ -122,7 +124,7 @@ export async function POST(request: NextRequest) {
|
|||
} = body;
|
||||
|
||||
// Check for duplicate title
|
||||
const existingTask = db.prepare('SELECT id FROM tasks WHERE title = ?').get(title);
|
||||
const existingTask = db.prepare('SELECT id FROM tasks WHERE title = ? AND workspace_id = ?').get(title, workspaceId);
|
||||
if (existingTask) {
|
||||
return NextResponse.json({ error: 'Task with this title already exists' }, { status: 409 });
|
||||
}
|
||||
|
|
@ -132,8 +134,8 @@ export async function POST(request: NextRequest) {
|
|||
const stmt = db.prepare(`
|
||||
INSERT INTO tasks (
|
||||
title, description, status, priority, assigned_to, created_by,
|
||||
created_at, updated_at, due_date, estimated_hours, tags, metadata
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
created_at, updated_at, due_date, estimated_hours, tags, metadata, workspace_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const dbResult = stmt.run(
|
||||
|
|
@ -148,7 +150,8 @@ export async function POST(request: NextRequest) {
|
|||
due_date,
|
||||
estimated_hours,
|
||||
JSON.stringify(tags),
|
||||
JSON.stringify(metadata)
|
||||
JSON.stringify(metadata),
|
||||
workspaceId
|
||||
);
|
||||
|
||||
const taskId = dbResult.lastInsertRowid as number;
|
||||
|
|
@ -159,27 +162,28 @@ export async function POST(request: NextRequest) {
|
|||
status,
|
||||
priority,
|
||||
assigned_to
|
||||
});
|
||||
}, workspaceId);
|
||||
|
||||
if (created_by) {
|
||||
db_helpers.ensureTaskSubscription(taskId, created_by)
|
||||
db_helpers.ensureTaskSubscription(taskId, created_by, workspaceId)
|
||||
}
|
||||
|
||||
// Create notification if assigned
|
||||
if (assigned_to) {
|
||||
db_helpers.ensureTaskSubscription(taskId, assigned_to)
|
||||
db_helpers.ensureTaskSubscription(taskId, assigned_to, workspaceId)
|
||||
db_helpers.createNotification(
|
||||
assigned_to,
|
||||
'assignment',
|
||||
'Task Assigned',
|
||||
`You have been assigned to task: ${title}`,
|
||||
'task',
|
||||
taskId
|
||||
taskId,
|
||||
workspaceId
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch the created task
|
||||
const createdTask = db.prepare('SELECT * FROM tasks WHERE id = ?').get(taskId) as Task;
|
||||
const createdTask = db.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?').get(taskId, workspaceId) as Task;
|
||||
const parsedTask = {
|
||||
...createdTask,
|
||||
tags: JSON.parse(createdTask.tags || '[]'),
|
||||
|
|
@ -208,6 +212,7 @@ export async function PUT(request: NextRequest) {
|
|||
|
||||
try {
|
||||
const db = getDatabase();
|
||||
const workspaceId = auth.user.workspace_id;
|
||||
const validated = await validateBody(request, bulkUpdateTaskStatusSchema);
|
||||
if ('error' in validated) return validated.error;
|
||||
const { tasks } = validated.data;
|
||||
|
|
@ -217,20 +222,21 @@ export async function PUT(request: NextRequest) {
|
|||
const updateStmt = db.prepare(`
|
||||
UPDATE tasks
|
||||
SET status = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
WHERE id = ? AND workspace_id = ?
|
||||
`);
|
||||
|
||||
const actor = auth.user.username
|
||||
|
||||
const transaction = db.transaction((tasksToUpdate: any[]) => {
|
||||
for (const task of tasksToUpdate) {
|
||||
const oldTask = db.prepare('SELECT * FROM tasks WHERE id = ?').get(task.id) as Task;
|
||||
const oldTask = db.prepare('SELECT * FROM tasks WHERE id = ? AND workspace_id = ?').get(task.id, workspaceId) as Task;
|
||||
if (!oldTask) continue;
|
||||
|
||||
if (task.status === 'done' && !hasAegisApproval(db, task.id)) {
|
||||
if (task.status === 'done' && !hasAegisApproval(db, task.id, workspaceId)) {
|
||||
throw new Error(`Aegis approval required for task ${task.id}`)
|
||||
}
|
||||
|
||||
updateStmt.run(task.status, now, task.id);
|
||||
updateStmt.run(task.status, now, task.id, workspaceId);
|
||||
|
||||
// Log status change if different
|
||||
if (oldTask && oldTask.status !== task.status) {
|
||||
|
|
@ -240,7 +246,8 @@ export async function PUT(request: NextRequest) {
|
|||
task.id,
|
||||
actor,
|
||||
`Task moved from ${oldTask.status} to ${task.status}`,
|
||||
{ oldStatus: oldTask.status, newStatus: task.status }
|
||||
{ oldStatus: oldTask.status, newStatus: task.status },
|
||||
workspaceId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
'use client'
|
||||
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
|
||||
interface MarkdownRendererProps {
|
||||
content: string
|
||||
preview?: boolean
|
||||
}
|
||||
|
||||
function getPreviewContent(content: string): string {
|
||||
const firstParagraph = content.trim().split(/\n\s*\n/)[0] || ''
|
||||
if (firstParagraph.length <= 240) return firstParagraph
|
||||
return `${firstParagraph.slice(0, 240)}...`
|
||||
}
|
||||
|
||||
export function MarkdownRenderer({ content, preview = false }: MarkdownRendererProps) {
|
||||
if (!content?.trim()) return null
|
||||
|
||||
const markdownContent = preview ? getPreviewContent(content) : content
|
||||
|
||||
return (
|
||||
<div className={`prose prose-invert max-w-none ${preview ? 'text-xs' : 'text-sm'}`}>
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
h1: ({ children }) => <h1 className={`${preview ? 'text-sm' : 'text-xl'} font-semibold mb-2`}>{children}</h1>,
|
||||
h2: ({ children }) => <h2 className={`${preview ? 'text-xs' : 'text-lg'} font-semibold mb-2`}>{children}</h2>,
|
||||
h3: ({ children }) => <h3 className={`${preview ? 'text-xs' : 'text-base'} font-semibold mb-1`}>{children}</h3>,
|
||||
p: ({ children }) => <p className={`text-foreground/85 ${preview ? 'text-xs mb-1' : 'text-sm mb-2'} leading-relaxed`}>{children}</p>,
|
||||
ul: ({ children }) => <ul className={`list-disc ml-4 ${preview ? 'text-xs mb-1' : 'text-sm mb-2'}`}>{children}</ul>,
|
||||
ol: ({ children }) => <ol className={`list-decimal ml-4 ${preview ? 'text-xs mb-1' : 'text-sm mb-2'}`}>{children}</ol>,
|
||||
li: ({ children }) => <li className="mb-0.5 text-foreground/85">{children}</li>,
|
||||
code: ({ children, className }) => {
|
||||
const isInline = !className
|
||||
if (isInline) {
|
||||
return <code className="bg-surface-2 text-primary px-1 py-0.5 rounded text-[0.85em]">{children}</code>
|
||||
}
|
||||
return (
|
||||
<code className="block bg-surface-2 border border-border rounded p-2 overflow-x-auto text-[0.85em]">
|
||||
{children}
|
||||
</code>
|
||||
)
|
||||
},
|
||||
blockquote: ({ children }) => (
|
||||
<blockquote className="border-l-2 border-border pl-3 italic text-muted-foreground mb-2">
|
||||
{children}
|
||||
</blockquote>
|
||||
),
|
||||
a: ({ href, children }) => (
|
||||
<a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:text-blue-300 underline">
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
strong: ({ children }) => <strong className="font-semibold text-foreground">{children}</strong>,
|
||||
em: ({ children }) => <em className="italic text-foreground/90">{children}</em>,
|
||||
}}
|
||||
>
|
||||
{markdownContent}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -808,6 +808,12 @@ const MODEL_TIER_LABELS: Record<string, string> = {
|
|||
haiku: 'Haiku $',
|
||||
}
|
||||
|
||||
const DEFAULT_MODEL_BY_TIER: Record<'opus' | 'sonnet' | 'haiku', string> = {
|
||||
opus: 'anthropic/claude-opus-4-5',
|
||||
sonnet: 'anthropic/claude-sonnet-4-20250514',
|
||||
haiku: 'anthropic/claude-haiku-4-5',
|
||||
}
|
||||
|
||||
// Enhanced Create Agent Modal with Template Wizard
|
||||
export function CreateAgentModal({
|
||||
onClose,
|
||||
|
|
@ -818,12 +824,14 @@ export function CreateAgentModal({
|
|||
}) {
|
||||
const [step, setStep] = useState<1 | 2 | 3>(1)
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<string | null>(null)
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([])
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
id: '',
|
||||
role: '',
|
||||
emoji: '',
|
||||
model: 'sonnet',
|
||||
modelTier: 'sonnet' as 'opus' | 'sonnet' | 'haiku',
|
||||
modelPrimary: DEFAULT_MODEL_BY_TIER.sonnet,
|
||||
workspaceAccess: 'rw' as 'rw' | 'ro' | 'none',
|
||||
sandboxMode: 'all' as 'all' | 'non-main',
|
||||
dockerNetwork: 'none' as 'none' | 'bridge',
|
||||
|
|
@ -841,6 +849,24 @@ export function CreateAgentModal({
|
|||
setFormData(prev => ({ ...prev, name, id }))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const loadAvailableModels = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/status?action=models')
|
||||
if (!response.ok) return
|
||||
const data = await response.json()
|
||||
const models = Array.isArray(data.models) ? data.models : []
|
||||
const names = models
|
||||
.map((model: any) => String(model.name || model.alias || '').trim())
|
||||
.filter(Boolean)
|
||||
setAvailableModels(Array.from(new Set<string>(names)))
|
||||
} catch {
|
||||
// Keep modal usable without model suggestions.
|
||||
}
|
||||
}
|
||||
loadAvailableModels()
|
||||
}, [])
|
||||
|
||||
// When template is selected, pre-fill form
|
||||
const selectTemplate = (type: string | null) => {
|
||||
setSelectedTemplate(type)
|
||||
|
|
@ -851,7 +877,8 @@ export function CreateAgentModal({
|
|||
...prev,
|
||||
role: tmpl.theme,
|
||||
emoji: tmpl.emoji,
|
||||
model: tmpl.modelTier === 'opus' ? 'opus' : tmpl.modelTier === 'haiku' ? 'haiku' : 'sonnet',
|
||||
modelTier: tmpl.modelTier,
|
||||
modelPrimary: DEFAULT_MODEL_BY_TIER[tmpl.modelTier],
|
||||
workspaceAccess: type === 'researcher' || type === 'content-creator' ? 'none' : type === 'reviewer' || type === 'security-auditor' ? 'ro' : 'rw',
|
||||
sandboxMode: type === 'orchestrator' ? 'non-main' : 'all',
|
||||
dockerNetwork: type === 'developer' || type === 'specialist-dev' ? 'bridge' : 'none',
|
||||
|
|
@ -868,6 +895,7 @@ export function CreateAgentModal({
|
|||
setIsCreating(true)
|
||||
setError(null)
|
||||
try {
|
||||
const primaryModel = formData.modelPrimary.trim() || DEFAULT_MODEL_BY_TIER[formData.modelTier]
|
||||
const response = await fetch('/api/agents', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
|
|
@ -878,7 +906,7 @@ export function CreateAgentModal({
|
|||
template: selectedTemplate || undefined,
|
||||
write_to_gateway: formData.write_to_gateway,
|
||||
gateway_config: {
|
||||
model: { primary: `anthropic/claude-${formData.model === 'opus' ? 'opus-4-5' : formData.model === 'haiku' ? 'haiku-4-5' : 'sonnet-4-20250514'}` },
|
||||
model: { primary: primaryModel },
|
||||
identity: { name: formData.name, theme: formData.role, emoji: formData.emoji },
|
||||
sandbox: {
|
||||
mode: formData.sandboxMode,
|
||||
|
|
@ -1033,14 +1061,18 @@ export function CreateAgentModal({
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-muted-foreground mb-1">Model</label>
|
||||
<label className="block text-sm text-muted-foreground mb-1">Model Tier</label>
|
||||
<div className="flex gap-2">
|
||||
{(['opus', 'sonnet', 'haiku'] as const).map(tier => (
|
||||
<button
|
||||
key={tier}
|
||||
onClick={() => setFormData(prev => ({ ...prev, model: tier }))}
|
||||
onClick={() => setFormData(prev => ({
|
||||
...prev,
|
||||
modelTier: tier,
|
||||
modelPrimary: DEFAULT_MODEL_BY_TIER[tier],
|
||||
}))}
|
||||
className={`flex-1 px-3 py-2 text-sm rounded-md border transition-smooth ${
|
||||
formData.model === tier ? MODEL_TIER_COLORS[tier] + ' border' : 'bg-surface-1 text-muted-foreground border-border'
|
||||
formData.modelTier === tier ? MODEL_TIER_COLORS[tier] + ' border' : 'bg-surface-1 text-muted-foreground border-border'
|
||||
}`}
|
||||
>
|
||||
{MODEL_TIER_LABELS[tier]}
|
||||
|
|
@ -1049,6 +1081,23 @@ export function CreateAgentModal({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm text-muted-foreground mb-1">Primary Model</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.modelPrimary}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, modelPrimary: e.target.value }))}
|
||||
list="create-agent-model-suggestions"
|
||||
className="w-full bg-surface-1 text-foreground border border-border rounded-md px-3 py-2 focus:outline-none focus:ring-1 focus:ring-primary/50 font-mono text-sm"
|
||||
placeholder={DEFAULT_MODEL_BY_TIER[formData.modelTier]}
|
||||
/>
|
||||
<datalist id="create-agent-model-suggestions">
|
||||
{availableModels.map((name) => (
|
||||
<option key={name} value={name} />
|
||||
))}
|
||||
</datalist>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-muted-foreground mb-1">Workspace</label>
|
||||
|
|
@ -1114,8 +1163,9 @@ export function CreateAgentModal({
|
|||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<div><span className="text-muted-foreground">ID:</span> <span className="text-foreground font-mono">{formData.id}</span></div>
|
||||
<div><span className="text-muted-foreground">Template:</span> <span className="text-foreground">{selectedTemplateData?.label || 'Custom'}</span></div>
|
||||
<div><span className="text-muted-foreground">Model:</span> <span className={`px-2 py-0.5 rounded text-xs ${MODEL_TIER_COLORS[formData.model]}`}>{MODEL_TIER_LABELS[formData.model]}</span></div>
|
||||
<div><span className="text-muted-foreground">Model:</span> <span className={`px-2 py-0.5 rounded text-xs ${MODEL_TIER_COLORS[formData.modelTier]}`}>{MODEL_TIER_LABELS[formData.modelTier]}</span></div>
|
||||
<div><span className="text-muted-foreground">Tools:</span> <span className="text-foreground">{selectedTemplateData?.toolCount || 'Custom'}</span></div>
|
||||
<div className="col-span-2"><span className="text-muted-foreground">Primary Model:</span> <span className="text-foreground font-mono">{formData.modelPrimary || DEFAULT_MODEL_BY_TIER[formData.modelTier]}</span></div>
|
||||
<div><span className="text-muted-foreground">Workspace:</span> <span className="text-foreground">{formData.workspaceAccess}</span></div>
|
||||
<div><span className="text-muted-foreground">Sandbox:</span> <span className="text-foreground">{formData.sandboxMode}</span></div>
|
||||
<div><span className="text-muted-foreground">Network:</span> <span className="text-foreground">{formData.dockerNetwork}</span></div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useSmartPoll } from '@/lib/use-smart-poll'
|
||||
import { AgentAvatar } from '@/components/ui/agent-avatar'
|
||||
import {
|
||||
OverviewTab,
|
||||
SoulTab,
|
||||
|
|
@ -319,9 +320,12 @@ export function AgentSquadPanelPhase3() {
|
|||
>
|
||||
{/* Agent Header */}
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-foreground text-lg">{agent.name}</h3>
|
||||
<p className="text-muted-foreground text-sm">{agent.role}</p>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<AgentAvatar name={agent.name} size="md" />
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-semibold text-foreground text-lg truncate">{agent.name}</h3>
|
||||
<p className="text-muted-foreground text-sm truncate">{agent.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,43 @@ interface NewJobForm {
|
|||
schedule: string
|
||||
command: string
|
||||
description: string
|
||||
model: string
|
||||
}
|
||||
|
||||
type CalendarViewMode = 'agenda' | 'day' | 'week' | 'month'
|
||||
|
||||
function startOfDay(date: Date): Date {
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
|
||||
}
|
||||
|
||||
function addDays(date: Date, days: number): Date {
|
||||
const next = new Date(date)
|
||||
next.setDate(next.getDate() + days)
|
||||
return next
|
||||
}
|
||||
|
||||
function isSameDay(a: Date, b: Date): boolean {
|
||||
return (
|
||||
a.getFullYear() === b.getFullYear() &&
|
||||
a.getMonth() === b.getMonth() &&
|
||||
a.getDate() === b.getDate()
|
||||
)
|
||||
}
|
||||
|
||||
function getWeekStart(date: Date): Date {
|
||||
const day = date.getDay()
|
||||
const diffToMonday = (day + 6) % 7
|
||||
return addDays(startOfDay(date), -diffToMonday)
|
||||
}
|
||||
|
||||
function getMonthStartGrid(date: Date): Date {
|
||||
const firstOfMonth = new Date(date.getFullYear(), date.getMonth(), 1)
|
||||
const day = firstOfMonth.getDay()
|
||||
return addDays(firstOfMonth, -day)
|
||||
}
|
||||
|
||||
function formatDateLabel(date: Date): string {
|
||||
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
export function CronManagementPanel() {
|
||||
|
|
@ -16,11 +53,18 @@ export function CronManagementPanel() {
|
|||
const [showAddForm, setShowAddForm] = useState(false)
|
||||
const [selectedJob, setSelectedJob] = useState<CronJob | null>(null)
|
||||
const [jobLogs, setJobLogs] = useState<any[]>([])
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([])
|
||||
const [calendarView, setCalendarView] = useState<CalendarViewMode>('week')
|
||||
const [calendarDate, setCalendarDate] = useState<Date>(startOfDay(new Date()))
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [agentFilter, setAgentFilter] = useState('all')
|
||||
const [stateFilter, setStateFilter] = useState<'all' | 'enabled' | 'disabled'>('all')
|
||||
const [newJob, setNewJob] = useState<NewJobForm>({
|
||||
name: '',
|
||||
schedule: '0 * * * *', // Every hour
|
||||
command: '',
|
||||
description: ''
|
||||
description: '',
|
||||
model: ''
|
||||
})
|
||||
|
||||
const formatRelativeTime = (timestamp: string | number, future = false) => {
|
||||
|
|
@ -56,6 +100,24 @@ export function CronManagementPanel() {
|
|||
loadCronJobs()
|
||||
}, [loadCronJobs])
|
||||
|
||||
useEffect(() => {
|
||||
const loadAvailableModels = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/status?action=models')
|
||||
if (!response.ok) return
|
||||
const data = await response.json()
|
||||
const models = Array.isArray(data.models) ? data.models : []
|
||||
const names = models
|
||||
.map((model: any) => String(model.name || model.alias || '').trim())
|
||||
.filter(Boolean)
|
||||
setAvailableModels(Array.from(new Set<string>(names)))
|
||||
} catch {
|
||||
// Keep cron form usable even when model discovery is unavailable.
|
||||
}
|
||||
}
|
||||
loadAvailableModels()
|
||||
}, [])
|
||||
|
||||
const loadJobLogs = async (jobName: string) => {
|
||||
try {
|
||||
const response = await fetch(`/api/cron?action=logs&job=${encodeURIComponent(jobName)}`)
|
||||
|
|
@ -130,7 +192,8 @@ export function CronManagementPanel() {
|
|||
action: 'add',
|
||||
jobName: newJob.name,
|
||||
schedule: newJob.schedule,
|
||||
command: newJob.command
|
||||
command: newJob.command,
|
||||
...(newJob.model.trim() ? { model: newJob.model.trim() } : {})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -139,7 +202,8 @@ export function CronManagementPanel() {
|
|||
name: '',
|
||||
schedule: '0 * * * *',
|
||||
command: '',
|
||||
description: ''
|
||||
description: '',
|
||||
model: ''
|
||||
})
|
||||
setShowAddForm(false)
|
||||
await loadCronJobs()
|
||||
|
|
@ -217,6 +281,82 @@ export function CronManagementPanel() {
|
|||
{ label: 'Monthly (1st)', value: '0 0 1 * *' },
|
||||
]
|
||||
|
||||
const uniqueAgents = Array.from(
|
||||
new Set(
|
||||
cronJobs
|
||||
.map((job) => (job.agentId || '').trim())
|
||||
.filter(Boolean)
|
||||
)
|
||||
)
|
||||
|
||||
const filteredJobs = cronJobs.filter((job) => {
|
||||
const query = searchQuery.trim().toLowerCase()
|
||||
const matchesQuery =
|
||||
!query ||
|
||||
job.name.toLowerCase().includes(query) ||
|
||||
job.command.toLowerCase().includes(query) ||
|
||||
(job.agentId || '').toLowerCase().includes(query) ||
|
||||
(job.model || '').toLowerCase().includes(query)
|
||||
|
||||
const matchesAgent = agentFilter === 'all' || (job.agentId || '') === agentFilter
|
||||
const matchesState =
|
||||
stateFilter === 'all' ||
|
||||
(stateFilter === 'enabled' && job.enabled) ||
|
||||
(stateFilter === 'disabled' && !job.enabled)
|
||||
|
||||
return matchesQuery && matchesAgent && matchesState
|
||||
})
|
||||
|
||||
const agendaJobs = [...filteredJobs].sort((a, b) => {
|
||||
const aRun = typeof a.nextRun === 'number' ? a.nextRun : Number.POSITIVE_INFINITY
|
||||
const bRun = typeof b.nextRun === 'number' ? b.nextRun : Number.POSITIVE_INFINITY
|
||||
return aRun - bRun
|
||||
})
|
||||
|
||||
const dayStart = startOfDay(calendarDate)
|
||||
const dayEnd = addDays(dayStart, 1)
|
||||
const dayJobs = filteredJobs
|
||||
.filter((job) => typeof job.nextRun === 'number' && job.nextRun >= dayStart.getTime() && job.nextRun < dayEnd.getTime())
|
||||
.sort((a, b) => (a.nextRun || 0) - (b.nextRun || 0))
|
||||
|
||||
const weekStart = getWeekStart(calendarDate)
|
||||
const weekDays = Array.from({ length: 7 }, (_, idx) => addDays(weekStart, idx))
|
||||
const jobsByWeekDay = weekDays.map((date) => {
|
||||
const start = startOfDay(date).getTime()
|
||||
const end = addDays(date, 1).getTime()
|
||||
const jobs = filteredJobs
|
||||
.filter((job) => typeof job.nextRun === 'number' && job.nextRun >= start && job.nextRun < end)
|
||||
.sort((a, b) => (a.nextRun || 0) - (b.nextRun || 0))
|
||||
return { date, jobs }
|
||||
})
|
||||
|
||||
const monthGridStart = getMonthStartGrid(calendarDate)
|
||||
const monthDays = Array.from({ length: 42 }, (_, idx) => addDays(monthGridStart, idx))
|
||||
const jobsByMonthDay = monthDays.map((date) => {
|
||||
const start = startOfDay(date).getTime()
|
||||
const end = addDays(date, 1).getTime()
|
||||
const jobs = filteredJobs
|
||||
.filter((job) => typeof job.nextRun === 'number' && job.nextRun >= start && job.nextRun < end)
|
||||
.sort((a, b) => (a.nextRun || 0) - (b.nextRun || 0))
|
||||
return { date, jobs }
|
||||
})
|
||||
|
||||
const moveCalendar = (direction: -1 | 1) => {
|
||||
setCalendarDate((prev) => {
|
||||
if (calendarView === 'day') return addDays(prev, direction)
|
||||
if (calendarView === 'week') return addDays(prev, direction * 7)
|
||||
if (calendarView === 'month') return new Date(prev.getFullYear(), prev.getMonth() + direction, 1)
|
||||
return addDays(prev, direction * 7)
|
||||
})
|
||||
}
|
||||
|
||||
const calendarRangeLabel =
|
||||
calendarView === 'day'
|
||||
? calendarDate.toLocaleDateString(undefined, { weekday: 'long', month: 'short', day: 'numeric', year: 'numeric' })
|
||||
: calendarView === 'week'
|
||||
? `${formatDateLabel(weekDays[0])} - ${formatDateLabel(weekDays[6])}`
|
||||
: calendarDate.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="border-b border-border pb-4">
|
||||
|
|
@ -246,6 +386,176 @@ export function CronManagementPanel() {
|
|||
</div>
|
||||
|
||||
<div className="grid lg:grid-cols-2 gap-6">
|
||||
{/* Calendar View - Phase A (read-only) */}
|
||||
<div className="lg:col-span-2 bg-card border border-border rounded-lg p-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Calendar View</h2>
|
||||
<p className="text-sm text-muted-foreground">Read-only schedule visibility across all cron jobs</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => moveCalendar(-1)}
|
||||
className="px-2 py-1.5 rounded border border-border text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors"
|
||||
>
|
||||
Prev
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCalendarDate(startOfDay(new Date()))}
|
||||
className="px-3 py-1.5 rounded border border-border text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors text-sm"
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
<button
|
||||
onClick={() => moveCalendar(1)}
|
||||
className="px-2 py-1.5 rounded border border-border text-muted-foreground hover:text-foreground hover:bg-secondary transition-colors"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
<div className="text-sm font-medium text-foreground ml-1">{calendarRangeLabel}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(['agenda', 'day', 'week', 'month'] as CalendarViewMode[]).map((mode) => (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => setCalendarView(mode)}
|
||||
className={`px-3 py-1.5 rounded text-sm border transition-colors ${
|
||||
calendarView === mode
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'border-border text-muted-foreground hover:text-foreground hover:bg-secondary'
|
||||
}`}
|
||||
>
|
||||
{mode === 'agenda' ? 'Agenda' : mode.charAt(0).toUpperCase() + mode.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-3">
|
||||
<input
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Search jobs, agents, models..."
|
||||
className="px-3 py-2 border border-border rounded-md bg-background text-foreground text-sm"
|
||||
/>
|
||||
<select
|
||||
value={agentFilter}
|
||||
onChange={(e) => setAgentFilter(e.target.value)}
|
||||
className="px-3 py-2 border border-border rounded-md bg-background text-foreground text-sm"
|
||||
>
|
||||
<option value="all">All Agents</option>
|
||||
{uniqueAgents.map((agentId) => (
|
||||
<option key={agentId} value={agentId}>
|
||||
{agentId}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={stateFilter}
|
||||
onChange={(e) => setStateFilter(e.target.value as 'all' | 'enabled' | 'disabled')}
|
||||
className="px-3 py-2 border border-border rounded-md bg-background text-foreground text-sm"
|
||||
>
|
||||
<option value="all">All States</option>
|
||||
<option value="enabled">Enabled</option>
|
||||
<option value="disabled">Disabled</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{calendarView === 'agenda' && (
|
||||
<div className="border border-border rounded-lg overflow-hidden">
|
||||
<div className="max-h-80 overflow-y-auto divide-y divide-border">
|
||||
{agendaJobs.length === 0 ? (
|
||||
<div className="p-4 text-sm text-muted-foreground">No jobs match the current filters.</div>
|
||||
) : (
|
||||
agendaJobs.map((job) => (
|
||||
<div key={`agenda-${job.id || job.name}`} className="p-3 flex flex-col md:flex-row md:items-center md:justify-between gap-2">
|
||||
<div>
|
||||
<div className="font-medium text-foreground">{job.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{job.agentId || 'system'} · {job.enabled ? 'enabled' : 'disabled'} · {job.schedule}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{job.nextRun ? new Date(job.nextRun).toLocaleString() : 'No upcoming run'}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{calendarView === 'day' && (
|
||||
<div className="border border-border rounded-lg p-3">
|
||||
{dayJobs.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground">No scheduled jobs for this day.</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{dayJobs.map((job) => (
|
||||
<div key={`day-${job.id || job.name}`} className="p-2 rounded border border-border bg-secondary/40">
|
||||
<div className="text-sm font-medium text-foreground">{job.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{job.nextRun ? new Date(job.nextRun).toLocaleTimeString() : 'Unknown time'} · {job.agentId || 'system'} · {job.enabled ? 'enabled' : 'disabled'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{calendarView === 'week' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-7 gap-2">
|
||||
{jobsByWeekDay.map(({ date, jobs }) => (
|
||||
<div key={`week-${date.toISOString()}`} className="border border-border rounded-lg p-2 min-h-36">
|
||||
<div className={`text-xs font-medium mb-2 ${isSameDay(date, new Date()) ? 'text-primary' : 'text-muted-foreground'}`}>
|
||||
{date.toLocaleDateString(undefined, { weekday: 'short', month: 'numeric', day: 'numeric' })}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{jobs.slice(0, 4).map((job) => (
|
||||
<div key={`week-job-${job.id || job.name}`} className="text-xs px-2 py-1 rounded bg-secondary text-foreground truncate" title={job.name}>
|
||||
{job.nextRun ? new Date(job.nextRun).toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' }) : '--:--'} {job.name}
|
||||
</div>
|
||||
))}
|
||||
{jobs.length > 4 && (
|
||||
<div className="text-xs text-muted-foreground">+{jobs.length - 4} more</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{calendarView === 'month' && (
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{jobsByMonthDay.map(({ date, jobs }) => {
|
||||
const inCurrentMonth = date.getMonth() === calendarDate.getMonth()
|
||||
return (
|
||||
<div
|
||||
key={`month-${date.toISOString()}`}
|
||||
className={`border border-border rounded-lg p-2 min-h-24 ${inCurrentMonth ? 'bg-transparent' : 'bg-secondary/30'}`}
|
||||
>
|
||||
<div className={`text-xs mb-1 ${isSameDay(date, new Date()) ? 'text-primary font-semibold' : inCurrentMonth ? 'text-foreground' : 'text-muted-foreground'}`}>
|
||||
{date.getDate()}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{jobs.slice(0, 2).map((job) => (
|
||||
<div key={`month-job-${job.id || job.name}`} className="text-[11px] px-1.5 py-0.5 rounded bg-secondary text-foreground truncate" title={job.name}>
|
||||
{job.name}
|
||||
</div>
|
||||
))}
|
||||
{jobs.length > 2 && <div className="text-[11px] text-muted-foreground">+{jobs.length - 2}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Job List */}
|
||||
<div className="bg-card border border-border rounded-lg p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Scheduled Jobs</h2>
|
||||
|
|
@ -304,6 +614,11 @@ export function CronManagementPanel() {
|
|||
<div className="text-sm text-muted-foreground mt-1 truncate">
|
||||
{job.command}
|
||||
</div>
|
||||
{job.model && (
|
||||
<div className="text-xs text-muted-foreground mt-1">
|
||||
Model: <span className="font-mono">{job.model}</span>
|
||||
</div>
|
||||
)}
|
||||
{job.lastRun && (
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
Last run: {formatRelativeTime(job.lastRun)}
|
||||
|
|
@ -368,6 +683,9 @@ export function CronManagementPanel() {
|
|||
<div className="bg-secondary rounded p-3 space-y-2 text-sm">
|
||||
<div><span className="text-muted-foreground">Schedule:</span> <code className="font-mono">{selectedJob.schedule}</code></div>
|
||||
<div><span className="text-muted-foreground">Command:</span> <code className="font-mono text-xs">{selectedJob.command}</code></div>
|
||||
{selectedJob.model && (
|
||||
<div><span className="text-muted-foreground">Model:</span> <code className="font-mono text-xs">{selectedJob.model}</code></div>
|
||||
)}
|
||||
<div><span className="text-muted-foreground">Status:</span> {selectedJob.enabled ? '🟢 Enabled' : '🔴 Disabled'}</div>
|
||||
{selectedJob.nextRun && (
|
||||
<div><span className="text-muted-foreground">Next run:</span> {new Date(selectedJob.nextRun).toLocaleString()}</div>
|
||||
|
|
@ -454,6 +772,26 @@ export function CronManagementPanel() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">Model (Optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newJob.model}
|
||||
onChange={(e) => setNewJob(prev => ({ ...prev, model: e.target.value }))}
|
||||
list="cron-model-suggestions"
|
||||
placeholder="anthropic/claude-sonnet-4-20250514"
|
||||
className="w-full px-3 py-2 border border-border rounded-md bg-background text-foreground font-mono text-sm"
|
||||
/>
|
||||
<datalist id="cron-model-suggestions">
|
||||
{availableModels.map((modelName) => (
|
||||
<option key={modelName} value={modelName} />
|
||||
))}
|
||||
</datalist>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
Leave empty to use the agent or gateway default model.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-foreground mb-2">Description (Optional)</label>
|
||||
<input
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useMissionControl } from '@/store'
|
||||
import { useSmartPoll } from '@/lib/use-smart-poll'
|
||||
import { AgentAvatar } from '@/components/ui/agent-avatar'
|
||||
import { MarkdownRenderer } from '@/components/markdown-renderer'
|
||||
|
||||
interface Task {
|
||||
id: number
|
||||
|
|
@ -360,13 +362,22 @@ export function TaskBoardPanel() {
|
|||
</div>
|
||||
|
||||
{task.description && (
|
||||
<p className="text-foreground/80 text-xs mb-2 line-clamp-2">
|
||||
{task.description}
|
||||
</p>
|
||||
<div className="mb-2 line-clamp-3 overflow-hidden">
|
||||
<MarkdownRenderer content={task.description} preview />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center text-xs text-muted-foreground">
|
||||
<span>{getAgentName(task.assigned_to)}</span>
|
||||
<span className="flex items-center gap-1.5 min-w-0">
|
||||
{task.assigned_to ? (
|
||||
<>
|
||||
<AgentAvatar name={getAgentName(task.assigned_to)} size="xs" />
|
||||
<span className="truncate">{getAgentName(task.assigned_to)}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>Unassigned</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="font-medium">{formatTaskTimestamp(task.created_at)}</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -620,7 +631,13 @@ function TaskDetailModal({
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-foreground/80 mb-4">{task.description || 'No description'}</p>
|
||||
{task.description ? (
|
||||
<div className="mb-4">
|
||||
<MarkdownRenderer content={task.description} />
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-foreground/80 mb-4">No description</p>
|
||||
)}
|
||||
<div className="flex gap-2 mt-4">
|
||||
{(['details', 'comments', 'quality'] as const).map(tab => (
|
||||
<button
|
||||
|
|
@ -647,7 +664,16 @@ function TaskDetailModal({
|
|||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Assigned to:</span>
|
||||
<span className="text-foreground ml-2">{task.assigned_to || 'Unassigned'}</span>
|
||||
<span className="text-foreground ml-2 inline-flex items-center gap-1.5">
|
||||
{task.assigned_to ? (
|
||||
<>
|
||||
<AgentAvatar name={task.assigned_to} size="xs" />
|
||||
<span>{task.assigned_to}</span>
|
||||
</>
|
||||
) : (
|
||||
<span>Unassigned</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Created:</span>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
'use client'
|
||||
|
||||
interface AgentAvatarProps {
|
||||
name: string
|
||||
size?: 'xs' | 'sm' | 'md'
|
||||
className?: string
|
||||
}
|
||||
|
||||
function getInitials(name: string): string {
|
||||
const parts = name
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
|
||||
if (parts.length === 0) return '?'
|
||||
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase()
|
||||
return `${parts[0][0] || ''}${parts[1][0] || ''}`.toUpperCase()
|
||||
}
|
||||
|
||||
function hashString(value: string): number {
|
||||
let hash = 0
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
hash = (hash * 31 + value.charCodeAt(i)) >>> 0
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
function getAvatarColors(name: string): { backgroundColor: string; color: string } {
|
||||
const hash = hashString(name.toLowerCase())
|
||||
const hue = hash % 360
|
||||
return {
|
||||
backgroundColor: `hsl(${hue} 70% 38%)`,
|
||||
color: 'hsl(0 0% 98%)',
|
||||
}
|
||||
}
|
||||
|
||||
const sizeClasses: Record<NonNullable<AgentAvatarProps['size']>, string> = {
|
||||
xs: 'w-5 h-5 text-[10px]',
|
||||
sm: 'w-6 h-6 text-[10px]',
|
||||
md: 'w-8 h-8 text-xs',
|
||||
}
|
||||
|
||||
export function AgentAvatar({ name, size = 'sm', className = '' }: AgentAvatarProps) {
|
||||
const initials = getInitials(name)
|
||||
const colors = getAvatarColors(name)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-full flex items-center justify-center font-semibold shrink-0 ${sizeClasses[size]} ${className}`}
|
||||
style={colors}
|
||||
title={name}
|
||||
aria-label={name}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ describe('logActivity', () => {
|
|||
|
||||
expect(mockPrepare).toHaveBeenCalled()
|
||||
expect(mockRun).toHaveBeenCalledWith(
|
||||
'task_created', 'task', 1, 'alice', 'Created task', null,
|
||||
'task_created', 'task', 1, 'alice', 'Created task', null, 1,
|
||||
)
|
||||
expect(mockBroadcast).toHaveBeenCalledWith(
|
||||
'activity.created',
|
||||
|
|
@ -105,7 +105,7 @@ describe('logActivity', () => {
|
|||
db_helpers.logActivity('update', 'agent', 2, 'bob', 'Updated agent', data)
|
||||
|
||||
expect(mockRun).toHaveBeenCalledWith(
|
||||
'update', 'agent', 2, 'bob', 'Updated agent', JSON.stringify(data),
|
||||
'update', 'agent', 2, 'bob', 'Updated agent', JSON.stringify(data), 1,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -119,7 +119,7 @@ describe('createNotification', () => {
|
|||
db_helpers.createNotification('alice', 'mention', 'Mentioned', 'You were mentioned')
|
||||
|
||||
expect(mockRun).toHaveBeenCalledWith(
|
||||
'alice', 'mention', 'Mentioned', 'You were mentioned', undefined, undefined,
|
||||
'alice', 'mention', 'Mentioned', 'You were mentioned', undefined, undefined, 1,
|
||||
)
|
||||
expect(mockBroadcast).toHaveBeenCalledWith(
|
||||
'notification.created',
|
||||
|
|
@ -135,7 +135,7 @@ describe('createNotification', () => {
|
|||
db_helpers.createNotification('bob', 'alert', 'Alert', 'CPU high', 'agent', 5)
|
||||
|
||||
expect(mockRun).toHaveBeenCalledWith(
|
||||
'bob', 'alert', 'Alert', 'CPU high', 'agent', 5,
|
||||
'bob', 'alert', 'Alert', 'CPU high', 'agent', 5, 1,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export interface User {
|
|||
username: string
|
||||
display_name: string
|
||||
role: 'admin' | 'operator' | 'viewer'
|
||||
workspace_id: number
|
||||
provider?: 'local' | 'google'
|
||||
email?: string | null
|
||||
avatar_url?: string | null
|
||||
|
|
@ -36,6 +37,7 @@ export interface UserSession {
|
|||
id: number
|
||||
token: string
|
||||
user_id: number
|
||||
workspace_id: number
|
||||
expires_at: number
|
||||
created_at: number
|
||||
ip_address: string | null
|
||||
|
|
@ -51,6 +53,7 @@ interface SessionQueryRow {
|
|||
email: string | null
|
||||
avatar_url: string | null
|
||||
is_approved: number
|
||||
workspace_id: number
|
||||
created_at: number
|
||||
updated_at: number
|
||||
last_login_at: number | null
|
||||
|
|
@ -66,6 +69,7 @@ interface UserQueryRow {
|
|||
email: string | null
|
||||
avatar_url: string | null
|
||||
is_approved: number
|
||||
workspace_id: number
|
||||
created_at: number
|
||||
updated_at: number
|
||||
last_login_at: number | null
|
||||
|
|
@ -75,16 +79,37 @@ interface UserQueryRow {
|
|||
// Session management
|
||||
const SESSION_DURATION = 7 * 24 * 60 * 60 // 7 days in seconds
|
||||
|
||||
export function createSession(userId: number, ipAddress?: string, userAgent?: string): { token: string; expiresAt: number } {
|
||||
function getDefaultWorkspaceId(): number {
|
||||
try {
|
||||
const db = getDatabase()
|
||||
const row = db.prepare(`SELECT id FROM workspaces WHERE slug = 'default' LIMIT 1`).get() as { id?: number } | undefined
|
||||
return row?.id || 1
|
||||
} catch {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkspaceIdFromRequest(request: Request): number {
|
||||
const user = getUserFromRequest(request)
|
||||
return user?.workspace_id || getDefaultWorkspaceId()
|
||||
}
|
||||
|
||||
export function createSession(
|
||||
userId: number,
|
||||
ipAddress?: string,
|
||||
userAgent?: string,
|
||||
workspaceId?: number
|
||||
): { token: string; expiresAt: number } {
|
||||
const db = getDatabase()
|
||||
const token = randomBytes(32).toString('hex')
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const expiresAt = now + SESSION_DURATION
|
||||
const resolvedWorkspaceId = workspaceId ?? ((db.prepare('SELECT workspace_id FROM users WHERE id = ?').get(userId) as { workspace_id?: number } | undefined)?.workspace_id || getDefaultWorkspaceId())
|
||||
|
||||
db.prepare(`
|
||||
INSERT INTO user_sessions (token, user_id, expires_at, ip_address, user_agent)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`).run(token, userId, expiresAt, ipAddress || null, userAgent || null)
|
||||
INSERT INTO user_sessions (token, user_id, expires_at, ip_address, user_agent, workspace_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
`).run(token, userId, expiresAt, ipAddress || null, userAgent || null, resolvedWorkspaceId)
|
||||
|
||||
// Update user's last login
|
||||
db.prepare('UPDATE users SET last_login_at = ?, updated_at = ? WHERE id = ?').run(now, now, userId)
|
||||
|
|
@ -101,7 +126,7 @@ export function validateSession(token: string): (User & { sessionId: number }) |
|
|||
const now = Math.floor(Date.now() / 1000)
|
||||
|
||||
const row = db.prepare(`
|
||||
SELECT u.id, u.username, u.display_name, u.role, u.provider, u.email, u.avatar_url, u.is_approved, u.created_at, u.updated_at, u.last_login_at,
|
||||
SELECT u.id, u.username, u.display_name, u.role, u.provider, u.email, u.avatar_url, u.is_approved, COALESCE(s.workspace_id, u.workspace_id, 1) as workspace_id, u.created_at, u.updated_at, u.last_login_at,
|
||||
s.id as session_id
|
||||
FROM user_sessions s
|
||||
JOIN users u ON u.id = s.user_id
|
||||
|
|
@ -115,6 +140,7 @@ export function validateSession(token: string): (User & { sessionId: number }) |
|
|||
username: row.username,
|
||||
display_name: row.display_name,
|
||||
role: row.role,
|
||||
workspace_id: row.workspace_id || getDefaultWorkspaceId(),
|
||||
provider: row.provider || 'local',
|
||||
email: row.email ?? null,
|
||||
avatar_url: row.avatar_url ?? null,
|
||||
|
|
@ -149,6 +175,7 @@ export function authenticateUser(username: string, password: string): User | nul
|
|||
username: row.username,
|
||||
display_name: row.display_name,
|
||||
role: row.role,
|
||||
workspace_id: row.workspace_id || getDefaultWorkspaceId(),
|
||||
provider: row.provider || 'local',
|
||||
email: row.email ?? null,
|
||||
avatar_url: row.avatar_url ?? null,
|
||||
|
|
@ -161,13 +188,13 @@ export function authenticateUser(username: string, password: string): User | nul
|
|||
|
||||
export function getUserById(id: number): User | null {
|
||||
const db = getDatabase()
|
||||
const row = db.prepare('SELECT id, username, display_name, role, provider, email, avatar_url, is_approved, created_at, updated_at, last_login_at FROM users WHERE id = ?').get(id) as User | undefined
|
||||
const row = db.prepare('SELECT id, username, display_name, role, workspace_id, provider, email, avatar_url, is_approved, created_at, updated_at, last_login_at FROM users WHERE id = ?').get(id) as User | undefined
|
||||
return row || null
|
||||
}
|
||||
|
||||
export function getAllUsers(): User[] {
|
||||
const db = getDatabase()
|
||||
return db.prepare('SELECT id, username, display_name, role, provider, email, avatar_url, is_approved, created_at, updated_at, last_login_at FROM users ORDER BY created_at').all() as User[]
|
||||
return db.prepare('SELECT id, username, display_name, role, workspace_id, provider, email, avatar_url, is_approved, created_at, updated_at, last_login_at FROM users ORDER BY created_at').all() as User[]
|
||||
}
|
||||
|
||||
export function createUser(
|
||||
|
|
@ -175,15 +202,16 @@ export function createUser(
|
|||
password: string,
|
||||
displayName: string,
|
||||
role: User['role'] = 'operator',
|
||||
options?: { provider?: 'local' | 'google'; provider_user_id?: string | null; email?: string | null; avatar_url?: string | null; is_approved?: 0 | 1; approved_by?: string | null; approved_at?: number | null }
|
||||
options?: { provider?: 'local' | 'google'; provider_user_id?: string | null; email?: string | null; avatar_url?: string | null; is_approved?: 0 | 1; approved_by?: string | null; approved_at?: number | null; workspace_id?: number }
|
||||
): User {
|
||||
const db = getDatabase()
|
||||
if (password.length < 12) throw new Error('Password must be at least 12 characters')
|
||||
const passwordHash = hashPassword(password)
|
||||
const provider = options?.provider || 'local'
|
||||
const workspaceId = options?.workspace_id || getDefaultWorkspaceId()
|
||||
const result = db.prepare(`
|
||||
INSERT INTO users (username, display_name, password_hash, role, provider, provider_user_id, email, avatar_url, is_approved, approved_by, approved_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO users (username, display_name, password_hash, role, provider, provider_user_id, email, avatar_url, is_approved, approved_by, approved_at, workspace_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
username,
|
||||
displayName,
|
||||
|
|
@ -196,6 +224,7 @@ export function createUser(
|
|||
typeof options?.is_approved === 'number' ? options.is_approved : 1,
|
||||
options?.approved_by || null,
|
||||
options?.approved_at || null,
|
||||
workspaceId,
|
||||
)
|
||||
|
||||
return getUserById(Number(result.lastInsertRowid))!
|
||||
|
|
@ -255,6 +284,7 @@ export function getUserFromRequest(request: Request): User | null {
|
|||
username: 'api',
|
||||
display_name: 'API Access',
|
||||
role: 'admin',
|
||||
workspace_id: getDefaultWorkspaceId(),
|
||||
created_at: 0,
|
||||
updated_at: 0,
|
||||
last_login_at: null,
|
||||
|
|
|
|||
|
|
@ -233,14 +233,22 @@ export const db_helpers = {
|
|||
/**
|
||||
* Log an activity to the activity stream
|
||||
*/
|
||||
logActivity: (type: string, entity_type: string, entity_id: number, actor: string, description: string, data?: any) => {
|
||||
logActivity: (
|
||||
type: string,
|
||||
entity_type: string,
|
||||
entity_id: number,
|
||||
actor: string,
|
||||
description: string,
|
||||
data?: any,
|
||||
workspaceId: number = 1
|
||||
) => {
|
||||
const db = getDatabase();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO activities (type, entity_type, entity_id, actor, description, data)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO activities (type, entity_type, entity_id, actor, description, data, workspace_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(type, entity_type, entity_id, actor, description, data ? JSON.stringify(data) : null);
|
||||
const result = stmt.run(type, entity_type, entity_id, actor, description, data ? JSON.stringify(data) : null, workspaceId);
|
||||
|
||||
const activityPayload = {
|
||||
id: result.lastInsertRowid,
|
||||
|
|
@ -251,6 +259,7 @@ export const db_helpers = {
|
|||
description,
|
||||
data: data || null,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
workspace_id: workspaceId,
|
||||
};
|
||||
|
||||
// Broadcast to SSE clients (webhooks listen here too)
|
||||
|
|
@ -260,14 +269,22 @@ export const db_helpers = {
|
|||
/**
|
||||
* Create notification for @mentions
|
||||
*/
|
||||
createNotification: (recipient: string, type: string, title: string, message: string, source_type?: string, source_id?: number) => {
|
||||
createNotification: (
|
||||
recipient: string,
|
||||
type: string,
|
||||
title: string,
|
||||
message: string,
|
||||
source_type?: string,
|
||||
source_id?: number,
|
||||
workspaceId: number = 1
|
||||
) => {
|
||||
const db = getDatabase();
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO notifications (recipient, type, title, message, source_type, source_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO notifications (recipient, type, title, message, source_type, source_id, workspace_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(recipient, type, title, message, source_type, source_id);
|
||||
const result = stmt.run(recipient, type, title, message, source_type, source_id, workspaceId);
|
||||
|
||||
const notificationPayload = {
|
||||
id: result.lastInsertRowid,
|
||||
|
|
@ -278,6 +295,7 @@ export const db_helpers = {
|
|||
source_type: source_type || null,
|
||||
source_id: source_id || null,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
workspace_id: workspaceId,
|
||||
};
|
||||
|
||||
// Broadcast to SSE clients (webhooks listen here too)
|
||||
|
|
@ -304,19 +322,19 @@ export const db_helpers = {
|
|||
/**
|
||||
* Update agent status and last seen
|
||||
*/
|
||||
updateAgentStatus: (agentName: string, status: Agent['status'], activity?: string) => {
|
||||
updateAgentStatus: (agentName: string, status: Agent['status'], activity?: string, workspaceId: number = 1) => {
|
||||
const db = getDatabase();
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
// Get agent ID before update
|
||||
const agent = db.prepare('SELECT id FROM agents WHERE name = ?').get(agentName) as { id: number } | undefined;
|
||||
const agent = db.prepare('SELECT id FROM agents WHERE name = ? AND workspace_id = ?').get(agentName, workspaceId) as { id: number } | undefined;
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE agents
|
||||
SET status = ?, last_seen = ?, last_activity = ?, updated_at = ?
|
||||
WHERE name = ?
|
||||
WHERE name = ? AND workspace_id = ?
|
||||
`);
|
||||
stmt.run(status, now, activity, now, agentName);
|
||||
stmt.run(status, now, activity, now, agentName, workspaceId);
|
||||
|
||||
// Broadcast agent status change to SSE clients
|
||||
if (agent) {
|
||||
|
|
@ -330,7 +348,7 @@ export const db_helpers = {
|
|||
}
|
||||
|
||||
// Log the status change
|
||||
db_helpers.logActivity('agent_status_change', 'agent', agent?.id || 0, agentName, `Agent status changed to ${status}`, { status, activity });
|
||||
db_helpers.logActivity('agent_status_change', 'agent', agent?.id || 0, agentName, `Agent status changed to ${status}`, { status, activity }, workspaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -350,52 +368,57 @@ export const db_helpers = {
|
|||
/**
|
||||
* Get unread notifications for recipient
|
||||
*/
|
||||
getUnreadNotifications: (recipient: string): Notification[] => {
|
||||
getUnreadNotifications: (recipient: string, workspaceId: number = 1): Notification[] => {
|
||||
const db = getDatabase();
|
||||
const stmt = db.prepare(`
|
||||
SELECT * FROM notifications
|
||||
WHERE recipient = ? AND read_at IS NULL
|
||||
WHERE recipient = ? AND read_at IS NULL AND workspace_id = ?
|
||||
ORDER BY created_at DESC
|
||||
`);
|
||||
|
||||
return stmt.all(recipient) as Notification[];
|
||||
return stmt.all(recipient, workspaceId) as Notification[];
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark notification as read
|
||||
*/
|
||||
markNotificationRead: (notificationId: number) => {
|
||||
markNotificationRead: (notificationId: number, workspaceId: number = 1) => {
|
||||
const db = getDatabase();
|
||||
const stmt = db.prepare(`
|
||||
UPDATE notifications
|
||||
SET read_at = ?
|
||||
WHERE id = ?
|
||||
WHERE id = ? AND workspace_id = ?
|
||||
`);
|
||||
|
||||
stmt.run(Math.floor(Date.now() / 1000), notificationId);
|
||||
stmt.run(Math.floor(Date.now() / 1000), notificationId, workspaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Ensure an agent is subscribed to a task
|
||||
*/
|
||||
ensureTaskSubscription: (taskId: number, agentName: string) => {
|
||||
ensureTaskSubscription: (taskId: number, agentName: string, workspaceId: number = 1) => {
|
||||
if (!agentName) return;
|
||||
const db = getDatabase();
|
||||
const stmt = db.prepare(`
|
||||
INSERT OR IGNORE INTO task_subscriptions (task_id, agent_name)
|
||||
VALUES (?, ?)
|
||||
SELECT t.id, ?
|
||||
FROM tasks t
|
||||
WHERE t.id = ? AND t.workspace_id = ?
|
||||
`);
|
||||
stmt.run(taskId, agentName);
|
||||
stmt.run(agentName, taskId, workspaceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get subscribers for a task
|
||||
*/
|
||||
getTaskSubscribers: (taskId: number): string[] => {
|
||||
getTaskSubscribers: (taskId: number, workspaceId: number = 1): string[] => {
|
||||
const db = getDatabase();
|
||||
const rows = db.prepare(`
|
||||
SELECT agent_name FROM task_subscriptions WHERE task_id = ?
|
||||
`).all(taskId) as Array<{ agent_name: string }>;
|
||||
SELECT ts.agent_name
|
||||
FROM task_subscriptions ts
|
||||
JOIN tasks t ON t.id = ts.task_id
|
||||
WHERE ts.task_id = ? AND t.workspace_id = ?
|
||||
`).all(taskId, workspaceId) as Array<{ agent_name: string }>;
|
||||
return rows.map((row) => row.agent_name);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -547,6 +547,102 @@ const migrations: Migration[] = [
|
|||
db.exec(`CREATE INDEX IF NOT EXISTS idx_claude_sessions_active ON claude_sessions(is_active) WHERE is_active = 1`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_claude_sessions_project ON claude_sessions(project_slug)`)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '021_workspace_isolation_phase1',
|
||||
up: (db) => {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS workspaces (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT NOT NULL UNIQUE,
|
||||
name TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
||||
);
|
||||
`)
|
||||
|
||||
db.prepare(`
|
||||
INSERT OR IGNORE INTO workspaces (id, slug, name, created_at, updated_at)
|
||||
VALUES (1, 'default', 'Default Workspace', unixepoch(), unixepoch())
|
||||
`).run()
|
||||
|
||||
const addWorkspaceIdColumn = (table: string) => {
|
||||
const tableExists = db
|
||||
.prepare(`SELECT 1 as ok FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
||||
.get(table) as { ok?: number } | undefined
|
||||
if (!tableExists?.ok) return
|
||||
|
||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>
|
||||
if (!cols.some((c) => c.name === 'workspace_id')) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN workspace_id INTEGER NOT NULL DEFAULT 1`)
|
||||
}
|
||||
db.exec(`UPDATE ${table} SET workspace_id = COALESCE(workspace_id, 1)`)
|
||||
}
|
||||
|
||||
const scopedTables = [
|
||||
'users',
|
||||
'user_sessions',
|
||||
'tasks',
|
||||
'agents',
|
||||
'comments',
|
||||
'activities',
|
||||
'notifications',
|
||||
'quality_reviews',
|
||||
'standup_reports',
|
||||
]
|
||||
|
||||
for (const table of scopedTables) {
|
||||
addWorkspaceIdColumn(table)
|
||||
}
|
||||
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_workspaces_slug ON workspaces(slug)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_users_workspace_id ON users(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_user_sessions_workspace_id ON user_sessions(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_workspace_id ON tasks(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_agents_workspace_id ON agents(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_comments_workspace_id ON comments(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_activities_workspace_id ON activities(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_notifications_workspace_id ON notifications(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_quality_reviews_workspace_id ON quality_reviews(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_standup_reports_workspace_id ON standup_reports(workspace_id)`)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '022_workspace_isolation_phase2',
|
||||
up: (db) => {
|
||||
const addWorkspaceIdColumn = (table: string) => {
|
||||
const tableExists = db
|
||||
.prepare(`SELECT 1 as ok FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
||||
.get(table) as { ok?: number } | undefined
|
||||
if (!tableExists?.ok) return
|
||||
|
||||
const cols = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>
|
||||
if (!cols.some((c) => c.name === 'workspace_id')) {
|
||||
db.exec(`ALTER TABLE ${table} ADD COLUMN workspace_id INTEGER NOT NULL DEFAULT 1`)
|
||||
}
|
||||
db.exec(`UPDATE ${table} SET workspace_id = COALESCE(workspace_id, 1)`)
|
||||
}
|
||||
|
||||
const scopedTables = [
|
||||
'messages',
|
||||
'alert_rules',
|
||||
'direct_connections',
|
||||
'github_syncs',
|
||||
'workflow_pipelines',
|
||||
'pipeline_runs',
|
||||
]
|
||||
|
||||
for (const table of scopedTables) {
|
||||
addWorkspaceIdColumn(table)
|
||||
}
|
||||
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_messages_workspace_id ON messages(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_alert_rules_workspace_id ON alert_rules(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_direct_connections_workspace_id ON direct_connections(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_github_syncs_workspace_id ON github_syncs(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_workflow_pipelines_workspace_id ON workflow_pipelines(workspace_id)`)
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_pipeline_runs_workspace_id ON pipeline_runs(workspace_id)`)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ export interface CronJob {
|
|||
name: string
|
||||
schedule: string
|
||||
command: string
|
||||
model?: string
|
||||
agentId?: string
|
||||
timezone?: string
|
||||
delivery?: string
|
||||
enabled: boolean
|
||||
lastRun?: number
|
||||
nextRun?: number
|
||||
|
|
|
|||
Loading…
Reference in New Issue