Compare commits
1 Commits
fix/subtas
...
82077d38e6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82077d38e6 |
@@ -1,44 +0,0 @@
|
||||
# Running Scrum Manager with Fermyon Spin
|
||||
|
||||
This project has been configured to run on [Fermyon Spin](https://developer.fermyon.com/spin/index), allowing for quick deployment as WebAssembly components.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Install Spin](https://developer.fermyon.com/spin/install) (v2.0 or later)
|
||||
- Node.js and npm
|
||||
|
||||
## Build
|
||||
|
||||
To build both the frontend and the Wasm-compatible backend:
|
||||
|
||||
```bash
|
||||
# Build Frontend (outputs to dist/)
|
||||
npm run build
|
||||
|
||||
# Build Backend (outputs to server/dist/spin.js)
|
||||
cd server
|
||||
npm install
|
||||
npm run build:spin
|
||||
cd ..
|
||||
```
|
||||
|
||||
## Running Locally
|
||||
|
||||
You can run the application locally using `spin up`.
|
||||
Note: The application requires a MySQL database. Spin connects to it via the address specified in `spin.toml`.
|
||||
|
||||
1. Ensure your MySQL database is running (e.g., via Docker).
|
||||
2. Run Spin:
|
||||
|
||||
```bash
|
||||
spin up --sqlite # If using SQLite support (not fully implemented yet, defaults to MySQL config in spin.toml)
|
||||
# OR for MySQL:
|
||||
spin up
|
||||
```
|
||||
*Note: You may need to adjust the `db_host` and credentials in `spin.toml` or via environment variables if your DB is not at localhost:3306.*
|
||||
|
||||
## Structure
|
||||
|
||||
- **`spin.toml`**: The Spin manifest file defining the application components.
|
||||
- **`server/app_spin.js`**: The Wasm entry point for the backend, using Hono.
|
||||
- **`server/db_spin.js`**: A database adapter adapting MySQL calls for the Spin environment.
|
||||
217
bin/LICENSE
217
bin/LICENSE
@@ -1,217 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (c) The Spin Framework Contributors. All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
--- LLVM Exceptions to the Apache 2.0 License ----
|
||||
|
||||
As an exception, if, as a result of your compiling your source code, portions
|
||||
of this Software are embedded into an Object form of such source code, you
|
||||
may redistribute such embedded portions in such Object form without complying
|
||||
with the conditions of Sections 4(a), 4(b) and 4(d) of the License.
|
||||
|
||||
In addition, if you combine or link compiled forms of this Software with
|
||||
software that is licensed under the GPLv2 ("Combined Software") and if a
|
||||
court of competent jurisdiction determines that the patent provision (Section
|
||||
3), the indemnity provision (Section 9) or other Section of the License
|
||||
conflicts with the conditions of the GPLv2, you may retroactively and
|
||||
prospectively choose to deem waived or otherwise exclude such Section(s) of
|
||||
the License, but only in their entirety and only with respect to the Combined
|
||||
Software.
|
||||
136
bin/README.md
136
bin/README.md
@@ -1,136 +0,0 @@
|
||||
<div align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="./docs/static/image/logo-dark.png">
|
||||
<img alt="spin logo" src="./docs/static/image/logo.png" width="300" height="128">
|
||||
</picture>
|
||||
<p>Spin is a framework for building, deploying, and running fast, secure, and composable cloud microservices with WebAssembly.</p>
|
||||
<a href="https://github.com/spinframework/spin/actions/workflows/build.yml"><img src="https://github.com/spinframework/spin/actions/workflows/build.yml/badge.svg" alt="build status" /></a>
|
||||
<a href="https://cloud-native.slack.com/archives/C089NJ9G1V0"><img alt="Slack" src="https://img.shields.io/badge/slack-spin-green.svg?logo=slack"></a>
|
||||
<a href="https://www.bestpractices.dev/projects/10373"><img src="https://www.bestpractices.dev/projects/10373/badge"></a>
|
||||
</div>
|
||||
|
||||
## What is Spin?
|
||||
|
||||
Spin is an open source framework for building and running fast, secure, and
|
||||
composable cloud microservices with WebAssembly. It aims to be the easiest way
|
||||
to get started with WebAssembly microservices, and takes advantage of the latest
|
||||
developments in the
|
||||
[WebAssembly component model](https://github.com/WebAssembly/component-model)
|
||||
and [Wasmtime](https://wasmtime.dev/) runtime.
|
||||
|
||||
Spin offers a simple CLI that helps you create, distribute, and execute
|
||||
applications, and in the next sections we will learn more about Spin
|
||||
applications and how to get started.
|
||||
|
||||
## Getting started
|
||||
|
||||
See the [Install Spin](https://spinframework.dev/install) page of the [Spin documentation](https://spinframework.dev) for a detailed
|
||||
guide on installing and configuring Spin, but in short run the following commands:
|
||||
```bash
|
||||
curl -fsSL https://spinframework.dev/downloads/install.sh | bash
|
||||
sudo mv ./spin /usr/local/bin/spin
|
||||
```
|
||||
|
||||
Alternatively, you could [build Spin from source](https://spinframework.dev/contributing-spin).
|
||||
|
||||
To get started writing apps, follow the [quickstart guide](https://spinframework.dev/quickstart/),
|
||||
and then follow the
|
||||
[Rust](https://spinframework.dev/rust-components/), [JavaScript](https://spinframework.dev/javascript-components), [Python](https://spinframework.dev/python-components), or [Go](https://spinframework.dev/go-components/)
|
||||
language guides, and the [guide on writing Spin applications](https://spinframework.dev/writing-apps/).
|
||||
|
||||
## Language support
|
||||
|
||||
WebAssembly is a language-agnostic runtime: you can build WebAssembly components from a variety of source languages. Spin SDKs are available for several languages, including:
|
||||
|
||||
* JavaScript: https://github.com/spinframework/spin-js-sdk
|
||||
* Rust: https://crates.io/crates/spin-sdk
|
||||
* Go: https://pkg.go.dev/github.com/fermyon/spin/sdk/go/v2
|
||||
* Python: https://github.com/spinframework/spin-python-sdk
|
||||
* Zig: https://github.com/dasimmet/zig-spin (third party)
|
||||
* Moonbit: https://github.com/gmlewis/spin-moonbit-sdk (third party)
|
||||
|
||||
> The Spin framework team supports the JavaScript, Rust, Go, and Python SDKs. Other language integrations are supported by their authors, and we're grateful to them for their work!
|
||||
|
||||
## Usage
|
||||
|
||||
Below is an example of using the `spin` CLI to create a new Spin application. To run the example you will need to install the `wasm32-wasip1` target for Rust.
|
||||
|
||||
```bash
|
||||
$ rustup target add wasm32-wasip1
|
||||
```
|
||||
|
||||
First, run the `spin new` command to create a Spin application from a template.
|
||||
```bash
|
||||
# Create a new Spin application named 'hello-rust' based on the Rust http template, accepting all defaults
|
||||
$ spin new --accept-defaults -t http-rust hello-rust
|
||||
```
|
||||
Running the `spin new` command created a `hello-rust` directory with all the necessary files for your application. Change to the `hello-rust` directory and build the application with `spin build`, then run it locally with `spin up`:
|
||||
|
||||
```bash
|
||||
# Compile to Wasm by executing the `build` command.
|
||||
$ spin build
|
||||
Executing the build command for component hello-rust: cargo build --target wasm32-wasip1 --release
|
||||
Finished release [optimized] target(s) in 0.03s
|
||||
Successfully ran the build command for the Spin components.
|
||||
|
||||
# Run the application locally.
|
||||
$ spin up
|
||||
Logging component stdio to ".spin/logs/"
|
||||
|
||||
Serving http://127.0.0.1:3000
|
||||
Available Routes:
|
||||
hello-rust: http://127.0.0.1:3000 (wildcard)
|
||||
```
|
||||
|
||||
That's it! Now that the application is running, use your browser or cURL in another shell to try it out:
|
||||
|
||||
```bash
|
||||
# Send a request to the application.
|
||||
$ curl -i 127.0.0.1:3000
|
||||
HTTP/1.1 200 OK
|
||||
content-type: text/plain
|
||||
transfer-encoding: chunked
|
||||
date: Sun, 02 Mar 2025 20:09:11 GMT
|
||||
|
||||
Hello World!
|
||||
```
|
||||
|
||||
You can make the app do more by editting the `src/lib.rs` file in the `hello-rust` directory using your favorite editor or IDE. To learn more about writing Spin applications see [Writing Applications](https://spinframework.dev/writing-apps) in the Spin documentation. To learn how to publish and distribute your application see the [Publishing and Distribution](https://spinframework.dev/distributing-apps) guide in the Spin documentation.
|
||||
|
||||
## Language Support for Spin Features
|
||||
|
||||
The table below summarizes the [feature support](https://spinframework.dev/language-support-overview) in each of the language SDKs.
|
||||
|
||||
| Feature | Rust SDK Supported? | TypeScript SDK Supported? | Python SDK Supported? | Tiny Go SDK Supported? | C# SDK Supported? |
|
||||
|-----|-----|-----|-----|-----|-----|
|
||||
| **Triggers** |
|
||||
| [HTTP](https://spinframework.dev/http-trigger) | Supported | Supported | Supported | Supported | Supported |
|
||||
| [Redis](https://spinframework.dev/redis-trigger) | Supported | Supported | Supported | Supported | Not Supported |
|
||||
| **APIs** |
|
||||
| [Outbound HTTP](https://spinframework.dev/rust-components.md#sending-outbound-http-requests) | Supported | Supported | Supported | Supported | Supported |
|
||||
| [Configuration Variables](https://spinframework.dev/variables) | Supported | Supported | Supported | Supported | Supported |
|
||||
| [Key Value Storage](https://spinframework.dev/kv-store-api-guide) | Supported | Supported | Supported | Supported | Not Supported |
|
||||
| [SQLite Storage](https://spinframework.dev/sqlite-api-guide) | Supported | Supported | Supported | Supported | Not Supported |
|
||||
| [MySQL](https://spinframework.dev/rdbms-storage#using-mysql-and-postgresql-from-applications) | Supported | Supported | Not Supported | Supported | Not Supported |
|
||||
| [PostgreSQL](https://spinframework.dev/rdbms-storage#using-mysql-and-postgresql-from-applications) | Supported | Supported | Not Supported | Supported | Supported |
|
||||
| [Outbound Redis](https://spinframework.dev/rust-components.md#storing-data-in-redis-from-rust-components) | Supported | Supported | Supported | Supported | Supported |
|
||||
| [Serverless AI](https://spinframework.dev/serverless-ai-api-guide) | Supported | Supported | Supported | Supported | Not Supported |
|
||||
| **Extensibility** |
|
||||
| [Authoring Custom Triggers](https://spinframework.dev/extending-and-embedding) | Supported | Not Supported | Not Supported | Not Supported | Not Supported |
|
||||
|
||||
## Getting Involved and Contributing
|
||||
|
||||
We are delighted that you are interested in making Spin better! Thank you!
|
||||
|
||||
Each Monday at 2:30pm UTC (odd weeks) and 9:00pm UTC (even weeks), we meet to discuss Spin issues, roadmap, and ideas in our Spin Project Meetings. Link to the meeting can be found in the Spin Project Meeting agenda below.
|
||||
|
||||
The [Spin Project Meeting agenda](https://docs.google.com/document/d/1EG392gb8Eg-1ZEPDy18pgFZvMMrdAEybpCSufFXoe00/edit?usp=sharing) is a public document. The document contains a rolling agenda with the date and time of each meeting, the Zoom link, and topics of discussion for the day. You will also find the meeting minutes for each meeting and the link to the recording. If you have something you would like to demo or discuss at the project meeting, we encourage you to add it to the agenda.
|
||||
|
||||
You can find the contributing guide [here](https://spinframework.dev/contributing-spin).
|
||||
|
||||
## Stay in Touch
|
||||
|
||||
Follow us on Twitter: [@spinframework](https://twitter.com/spinframework)
|
||||
|
||||
You can join the Spin community in the [Spin CNCF Slack channel](https://cloud-native.slack.com/archives/C089NJ9G1V0) where you can ask questions, get help, and show off the cool things you are doing with Spin!
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUd0VENDQmpxZ0F3SUJBZ0lVWWtEZ21PZFRyN2JnUlIzdGZSR29IMTRHZzNRd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpZd01qRXdNVGd4TkRNM1doY05Nall3TWpFd01UZ3lORE0zV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVaekN3SElaSlJlaUNVV3JaZFgvRkRTNEFuVjVmNGxNUXV6NEcKVVBuNnF1a3A5ay8ycWdlM1JMOW4zNFF5d1VVUkt4Y3FQaG1IU3RRaFFTUkdaYXhzWWFPQ0JWa3dnZ1ZWTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVONGJoCmdjUVlaNFhZSDhFU0ZscUQ1cWVNUVg4d0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d1lnWURWUjBSQVFIL0JGZ3dWb1pVYUhSMGNITTZMeTluYVhSb2RXSXVZMjl0TDNOd2FXNW1jbUZ0WlhkdgpjbXN2YzNCcGJpOHVaMmwwYUhWaUwzZHZjbXRtYkc5M2N5OXlaV3hsWVhObExubHRiRUJ5WldaekwzUmhaM012CmRqTXVOaTR3TURrR0Npc0dBUVFCZzc4d0FRRUVLMmgwZEhCek9pOHZkRzlyWlc0dVlXTjBhVzl1Y3k1bmFYUm8KZFdKMWMyVnlZMjl1ZEdWdWRDNWpiMjB3RWdZS0t3WUJCQUdEdnpBQkFnUUVjSFZ6YURBMkJnb3JCZ0VFQVlPLwpNQUVEQkNoak5XTmlNek0wWkdVeU1HSXpaRGt3TldRd016YzNOamMzTldKbFpUYzJPR1V6TnpSaVltSXlNQlVHCkNpc0dBUVFCZzc4d0FRUUVCMUpsYkdWaGMyVXdJQVlLS3dZQkJBR0R2ekFCQlFRU2MzQnBibVp5WVcxbGQyOXkKYXk5emNHbHVNQjRHQ2lzR0FRUUJnNzh3QVFZRUVISmxabk12ZEdGbmN5OTJNeTQyTGpBd093WUtLd1lCQkFHRAp2ekFCQ0FRdERDdG9kSFJ3Y3pvdkwzUnZhMlZ1TG1GamRHbHZibk11WjJsMGFIVmlkWE5sY21OdmJuUmxiblF1ClkyOXRNR1FHQ2lzR0FRUUJnNzh3QVFrRVZneFVhSFIwY0hNNkx5OW5hWFJvZFdJdVkyOXRMM053YVc1bWNtRnQKWlhkdmNtc3ZjM0JwYmk4dVoybDBhSFZpTDNkdmNtdG1iRzkzY3k5eVpXeGxZWE5sTG5sdGJFQnlaV1p6TDNSaApaM012ZGpNdU5pNHdNRGdHQ2lzR0FRUUJnNzh3QVFvRUtnd29ZelZqWWpNek5HUmxNakJpTTJRNU1EVmtNRE0zCk56WTNOelZpWldVM05qaGxNemMwWW1KaU1qQWRCZ29yQmdFRUFZTy9NQUVMQkE4TURXZHBkR2gxWWkxb2IzTjAKWldRd05RWUtLd1lCQkFHRHZ6QUJEQVFuRENWb2RIUndjem92TDJkcGRHaDFZaTVqYjIwdmMzQnBibVp5WVcxbApkMjl5YXk5emNHbHVNRGdHQ2lzR0FRUUJnNzh3QVEwRUtnd29ZelZqWWpNek5HUmxNakJpTTJRNU1EVmtNRE0zCk56WTNOelZpWldVM05qaGxNemMwWW1KaU1qQWdCZ29yQmdFRUFZTy9NQUVPQkJJTUVISmxabk12ZEdGbmN5OTIKTXk0MkxqQXdHUVlLS3dZQkJBR0R2ekFCRHdRTERBazBNak0yTnprMk5qUXdNQVlLS3dZQkJBR0R2ekFCRUFRaQpEQ0JvZEhSd2N6b3ZMMmRwZEdoMVlpNWpiMjB2YzNCcGJtWnlZVzFsZDI5eWF6QVpCZ29yQmdFRUFZTy9NQUVSCkJBc01DVEU1TlRrM01qVTJOakJrQmdvckJnRUVBWU8vTUFFU0JGWU1WR2gwZEhCek9pOHZaMmwwYUhWaUxtTnYKYlM5emNHbHVabkpoYldWM2IzSnJMM053YVc0dkxtZHBkR2gxWWk5M2IzSnJabXh2ZDNNdmNtVnNaV0Z6WlM1NQpiV3hBY21WbWN5OTBZV2R6TDNZekxqWXVNREE0QmdvckJnRUVBWU8vTUFFVEJDb01LR00xWTJJek16UmtaVEl3CllqTmtPVEExWkRBek56YzJOemMxWW1WbE56WTRaVE0zTkdKaVlqSXdGQVlLS3dZQkJBR0R2ekFCRkFRR0RBUncKZFhOb01Ga0dDaXNHQVFRQmc3OHdBUlVFU3d4SmFIUjBjSE02THk5bmFYUm9kV0l1WTI5dEwzTndhVzVtY21GdApaWGR2Y21zdmMzQnBiaTloWTNScGIyNXpMM0oxYm5Ndk1qRTROell6TlRFek1qVXZZWFIwWlcxd2RITXZNVEFXCkJnb3JCZ0VFQVlPL01BRVdCQWdNQm5CMVlteHBZekNCaVFZS0t3WUJCQUhXZVFJRUFnUjdCSGtBZHdCMUFOMDkKTUdyR3h4RXlZeGtlSEpsbk53S2lTbDY0M2p5dC80ZUtjb0F2S2U2T0FBQUJuRWpETDg0QUFBUURBRVl3UkFJZwpPYll6cWFxWEptYlZKM0FGd2txMzgrWnlSWE9mQm9EdlZtQnpRbkczcmJVQ0lFSzdFdDRqZFhZZWUreExBMktNCnBRSVBKRkZXQURlNUNUOHZIcDBzWkJBUE1Bb0dDQ3FHU000OUJBTURBMmtBTUdZQ01RRHNFWHdjdVJ4ajZNeFUKa2RvRWVpMkVjR3U4Y0w0Ty9XRVo0N3d3ajZ6Nnp0OGhmYlZFcWo2RUN6VEVycHJmYys4Q01RRGZrdTNxK1VKdQpLczVhRGtOU1piYkFXbzdmQ2lzYjlKbkNPYUYzS0I5Z0tOTjZIWWh3MGpXaVpSdnd1Q1FhV240PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
|
||||
@@ -1 +0,0 @@
|
||||
MEQCIBOrg4FEuMQ1Lc1kJbUqE1rd+iEvE1VBAdv8lHKueZ42AiBPdsJTq2CDpRKmNt8kiPBSMW6YI3DpTTVywFg1o4pUVQ==
|
||||
26
k8s/overlays/on-premise/ingress.yaml
Normal file
26
k8s/overlays/on-premise/ingress.yaml
Normal file
@@ -0,0 +1,26 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: scrum-manager-ingress
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
rules:
|
||||
- host: scrum.local
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: frontend
|
||||
port:
|
||||
number: 80
|
||||
- path: /api
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: backend
|
||||
port:
|
||||
number: 3001
|
||||
13
k8s/overlays/on-premise/kustomization.yaml
Normal file
13
k8s/overlays/on-premise/kustomization.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
- mysql-pv.yaml
|
||||
- ingress.yaml
|
||||
|
||||
patches:
|
||||
- path: mysql-pvc-patch.yaml
|
||||
target:
|
||||
kind: PersistentVolumeClaim
|
||||
name: mysql-data-pvc
|
||||
14
k8s/overlays/on-premise/mysql-pv.yaml
Normal file
14
k8s/overlays/on-premise/mysql-pv.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: mysql-pv
|
||||
labels:
|
||||
type: local
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
hostPath:
|
||||
path: "/mnt/data/mysql"
|
||||
7
k8s/overlays/on-premise/mysql-pvc-patch.yaml
Normal file
7
k8s/overlays/on-premise/mysql-pvc-patch.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mysql-data-pvc
|
||||
spec:
|
||||
storageClassName: manual
|
||||
volumeName: mysql-pv
|
||||
86
package-lock.json
generated
86
package-lock.json
generated
@@ -10,7 +10,8 @@
|
||||
"dependencies": {
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"recharts": "^3.7.0"
|
||||
"recharts": "^3.7.0",
|
||||
"socket.io-client": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
@@ -1542,6 +1543,11 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
|
||||
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
@@ -2631,7 +2637,6 @@
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
@@ -2683,6 +2688,26 @@
|
||||
"integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.6.4",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
|
||||
"integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.18.3",
|
||||
"xmlhttprequest-ssl": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
@@ -3485,8 +3510,7 @@
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
@@ -3988,6 +4012,32 @@
|
||||
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.8.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
|
||||
"integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1",
|
||||
"engine.io-client": "~6.6.1",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
|
||||
"integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -4525,6 +4575,26 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
@@ -4540,6 +4610,14 @@
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
|
||||
"integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
"dependencies": {
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"recharts": "^3.7.0"
|
||||
"recharts": "^3.7.0",
|
||||
"socket.io-client": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
async function handleRequest(req, res) {
|
||||
console.log("Handle called");
|
||||
return {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/plain' },
|
||||
body: 'Hello from Spin Wasm (Corrected Export)!'
|
||||
};
|
||||
}
|
||||
|
||||
export const incomingHandler = {
|
||||
handle: handleRequest
|
||||
};
|
||||
|
||||
// Keep default just in case, but incomingHandler is key
|
||||
export default handleRequest;
|
||||
@@ -1,137 +0,0 @@
|
||||
import { Hono } from 'hono';
|
||||
import { handleRequest } from '@fermyon/spin-sdk';
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// Middleware to mock Express request/response specific methods if needed
|
||||
// Or we just rewrite routes to use Hono context.
|
||||
|
||||
// Since rewriting all routes is heavy, let's try to mount simple wrappers
|
||||
// or just import the router logic if we refactor routes.
|
||||
// Given the implementation plan said "Re-implement routing logic",
|
||||
// and the routes are currently Express routers, we probably need to wrap them
|
||||
// or quickly rewrite them to Hono.
|
||||
|
||||
// Strategy: Import the routes and mount them.
|
||||
// BUT Express routers won't work in Hono.
|
||||
// We must rewrite the route definitions in this file or transformed files.
|
||||
// For "quick deployment", I will inline the mounting of existing logic where possible,
|
||||
// using the db_spin adapter.
|
||||
|
||||
import pool from './db_spin.js';
|
||||
import bcrypt from 'bcryptjs';
|
||||
// import { randomUUID } from 'crypto'; // Use global crypto
|
||||
const randomUUID = () => crypto.randomUUID();
|
||||
|
||||
// --- AUTH ROUTES ---
|
||||
app.post('/api/auth/login', async (c) => {
|
||||
try {
|
||||
const { email, password } = await c.req.json();
|
||||
if (!email || !password) return c.json({ error: 'Email and password required' }, 400);
|
||||
|
||||
const [rows] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
|
||||
if (rows.length === 0) return c.json({ error: 'Invalid email or password' }, 401);
|
||||
|
||||
const user = rows[0];
|
||||
const valid = await bcrypt.compare(password, user.password_hash);
|
||||
if (!valid) return c.json({ error: 'Invalid email or password' }, 401);
|
||||
|
||||
return c.json({
|
||||
id: user.id, name: user.name, role: user.role, email: user.email,
|
||||
color: user.color, avatar: user.avatar, dept: user.dept,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/register', async (c) => {
|
||||
try {
|
||||
const { name, email, password, role, dept } = await c.req.json();
|
||||
if (!name || !email || !password) return c.json({ error: 'Required fields missing' }, 400);
|
||||
|
||||
const [existing] = await pool.query('SELECT id FROM users WHERE email = ?', [email]);
|
||||
if (existing.length > 0) return c.json({ error: 'Email already registered' }, 409);
|
||||
|
||||
const id = randomUUID();
|
||||
const password_hash = await bcrypt.hash(password, 10);
|
||||
// ... (simplified avatar logic)
|
||||
const avatar = name.substring(0, 2).toUpperCase();
|
||||
const color = '#818cf8';
|
||||
|
||||
await pool.query(
|
||||
'INSERT INTO users (id, name, role, email, password_hash, color, avatar, dept) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[id, name, role || 'employee', email, password_hash, color, avatar, dept || '']
|
||||
);
|
||||
return c.json({ id, name, role: role || 'employee', email, color, avatar, dept: dept || '' }, 201);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// --- TASKS ROUTES (Simplified for Wasm demo) ---
|
||||
async function getFullTask(taskId) {
|
||||
const [taskRows] = await pool.query('SELECT * FROM tasks WHERE id = ?', [taskId]);
|
||||
if (taskRows.length === 0) return null;
|
||||
const task = taskRows[0];
|
||||
// For brevity, not fetching sub-resources in this quick conversion,
|
||||
// but in full prod we would.
|
||||
// ... complete implementation would replicate existing logic ...
|
||||
return task;
|
||||
}
|
||||
|
||||
app.get('/api/tasks', async (c) => {
|
||||
try {
|
||||
const [rows] = await pool.query('SELECT * FROM tasks ORDER BY created_at DESC');
|
||||
// Simplify for now: Just return tasks
|
||||
return c.json(rows);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/tasks', async (c) => {
|
||||
try {
|
||||
const { title, description, status, priority, assignee, reporter, dueDate } = await c.req.json();
|
||||
const id = randomUUID();
|
||||
await pool.query(
|
||||
'INSERT INTO tasks (id, title, description, status, priority, assignee_id, reporter_id, due_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[id, title, description || '', status || 'todo', priority || 'medium', assignee || null, reporter || null, dueDate || null]
|
||||
);
|
||||
return c.json({ id, title, status }, 201);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return c.json({ error: 'Internal server error' }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (c) => c.json({ status: 'ok', engine: 'spin-wasm' }));
|
||||
|
||||
// Export the Spin handler
|
||||
export const spinHandler = async (req, res) => {
|
||||
// Spin generic handler interacting with Hono?
|
||||
// Hono has a fetch method: app.fetch(request, env, ctx)
|
||||
// Spin request is slightly different, but let's see if we can adapt.
|
||||
// Actually, Spin JS SDK v2 exports `handleRequest` which takes (request, response).
|
||||
// Hono might need an adapter.
|
||||
|
||||
// Simple adapter for Hono .fetch to Spin
|
||||
// Construct standard Request object from Spin calls if needed,
|
||||
// but simpler to use Hono's handle() if passing standard web standards.
|
||||
|
||||
// Assuming standard web signature is passed by recent Spin SDKs or we use 'node-adapter' if built via bundling.
|
||||
// But since we are likely using a bundler, strict Spin API is:
|
||||
// export async function handleRequest(request: Request): Promise<Response>
|
||||
|
||||
return app.fetch(req);
|
||||
};
|
||||
|
||||
// If using valid Spin JS plugin that looks for `handleRequest` as default export or named export
|
||||
// We will export it as `handleRequest` (default)
|
||||
export default async function (req) {
|
||||
return await app.fetch(req);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { build } from 'esbuild';
|
||||
import { SpinEsbuildPlugin } from "@spinframework/build-tools/plugins/esbuild/index.js";
|
||||
|
||||
const spinPlugin = await SpinEsbuildPlugin();
|
||||
|
||||
await build({
|
||||
entryPoints: ['./app_spin.js'],
|
||||
outfile: './dist/spin.js',
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
sourcemap: false,
|
||||
minify: false,
|
||||
plugins: [spinPlugin],
|
||||
target: 'es2020',
|
||||
external: ['fermyon:*', 'spin:*'],
|
||||
});
|
||||
14
server/db.js
14
server/db.js
@@ -98,6 +98,20 @@ export async function initDB() {
|
||||
)
|
||||
`);
|
||||
|
||||
await conn.query(`
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
user_id VARCHAR(36) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
message TEXT,
|
||||
link VARCHAR(255),
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`);
|
||||
|
||||
console.log('✅ Database tables initialized');
|
||||
} finally {
|
||||
conn.release();
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import { Mysql } from '@fermyon/spin-sdk';
|
||||
|
||||
const getEnv = (key, def) => {
|
||||
try {
|
||||
return (typeof process !== 'undefined' && process.env && process.env[key]) || def;
|
||||
} catch {
|
||||
return def;
|
||||
}
|
||||
};
|
||||
|
||||
const DB_URL = `mysql://${getEnv('DB_USER', 'root')}:${getEnv('DB_PASSWORD', 'scrumpass')}@${getEnv('DB_HOST', 'localhost')}:${getEnv('DB_PORT', '3306')}/${getEnv('DB_NAME', 'scrum_manager')}`;
|
||||
|
||||
function rowToObject(row, columns) {
|
||||
const obj = {};
|
||||
columns.forEach((col, index) => {
|
||||
obj[col.name] = row[index];
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
class SpinConnection {
|
||||
constructor(conn) {
|
||||
this.conn = conn;
|
||||
}
|
||||
|
||||
async query(sql, params = []) {
|
||||
console.log('SpinDB Query:', sql, params);
|
||||
try {
|
||||
const result = this.conn.query(sql, params);
|
||||
|
||||
const rows = result.rows.map(r => rowToObject(r, result.columns));
|
||||
const fields = result.columns.map(c => ({ name: c.name }));
|
||||
|
||||
if (sql.trim().toUpperCase().startsWith('INSERT') || sql.trim().toUpperCase().startsWith('UPDATE') || sql.trim().toUpperCase().startsWith('DELETE')) {
|
||||
return [{ affectedRows: result.rowsAffected || 0, insertId: result.lastInsertId || 0 }, fields];
|
||||
}
|
||||
|
||||
return [rows, fields];
|
||||
} catch (e) {
|
||||
console.error('SpinDB Error:', e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async beginTransaction() {
|
||||
try {
|
||||
this.conn.query('START TRANSACTION', []);
|
||||
} catch (e) {
|
||||
console.warn('Transaction start failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async commit() {
|
||||
try { this.conn.query('COMMIT', []); } catch (e) { }
|
||||
}
|
||||
|
||||
async rollback() {
|
||||
try { this.conn.query('ROLLBACK', []); } catch (e) { }
|
||||
}
|
||||
|
||||
release() { }
|
||||
}
|
||||
|
||||
export const initDB = async () => {
|
||||
console.log('Spin DB adapter ready.');
|
||||
};
|
||||
|
||||
// Lazy initialization to avoid Wizer issues
|
||||
let poolInstance = null;
|
||||
function getPool() {
|
||||
if (!poolInstance) {
|
||||
poolInstance = {
|
||||
async getConnection() {
|
||||
const conn = Mysql.open(DB_URL);
|
||||
return new SpinConnection(conn);
|
||||
},
|
||||
async query(sql, params) {
|
||||
const conn = await this.getConnection();
|
||||
const result = await conn.query(sql, params);
|
||||
return result;
|
||||
},
|
||||
escape: (val) => `'${val}'`,
|
||||
end: () => { }
|
||||
};
|
||||
}
|
||||
return poolInstance;
|
||||
}
|
||||
|
||||
export default {
|
||||
query: (sql, params) => getPool().query(sql, params),
|
||||
getConnection: () => getPool().getConnection(),
|
||||
end: () => getPool().end(),
|
||||
initDB
|
||||
};
|
||||
@@ -4,31 +4,57 @@ import { initDB } from './db.js';
|
||||
import authRoutes from './routes/auth.js';
|
||||
import taskRoutes from './routes/tasks.js';
|
||||
import exportRoutes from './routes/export.js';
|
||||
import notificationRoutes from './routes/notifications.js';
|
||||
|
||||
import { createServer } from 'http';
|
||||
import { Server } from 'socket.io';
|
||||
|
||||
const app = express();
|
||||
const httpServer = createServer(app);
|
||||
const io = new Server(httpServer, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"]
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Socket.io connection handling
|
||||
io.on('connection', (socket) => {
|
||||
socket.on('join', (userId) => {
|
||||
socket.join(userId);
|
||||
console.log(`User ${userId} joined notification room`);
|
||||
});
|
||||
});
|
||||
|
||||
// Middleware to attach io to req
|
||||
app.use((req, res, next) => {
|
||||
req.io = io;
|
||||
next();
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/tasks', taskRoutes);
|
||||
app.use('/api/export', exportRoutes);
|
||||
app.use('/api/notifications', notificationRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (_req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Initialize DB and start server
|
||||
// Initialize DB and start server
|
||||
async function start() {
|
||||
try {
|
||||
await initDB();
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`🚀 Backend server running on port ${PORT}`);
|
||||
httpServer.listen(PORT, () => {
|
||||
console.log(`🚀 Backend server running on port ${PORT} with Socket.io`);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -41,4 +67,4 @@ if (process.env.NODE_ENV !== 'test') {
|
||||
start();
|
||||
}
|
||||
|
||||
export { app, start };
|
||||
export { app, start, io };
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"packages": {
|
||||
"@fermyon/spin-sdk": {
|
||||
"witPath": "../../bin/wit",
|
||||
"world": "spin-imports"
|
||||
}
|
||||
},
|
||||
"project": {},
|
||||
"version": 1
|
||||
}
|
||||
3139
server/package-lock.json
generated
3139
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -5,21 +5,16 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "node index.js",
|
||||
"test": "vitest",
|
||||
"build:spin": "node build.mjs && node node_modules/@fermyon/spin-sdk/bin/j2w.mjs -i dist/spin.js -o dist/main.wasm --trigger-type spin3-http"
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fermyon/spin-sdk": "^2.2.0",
|
||||
"@spinframework/wasi-http-proxy": "^1.0.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"hono": "^4.6.14",
|
||||
"mysql2": "^3.14.1"
|
||||
"mysql2": "^3.14.1",
|
||||
"socket.io": "^4.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@spinframework/build-tools": "^1.0.7",
|
||||
"esbuild": "^0.24.2",
|
||||
"supertest": "^7.2.2",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Polyfill for crypto in Wizer environment
|
||||
if (!globalThis.crypto) {
|
||||
globalThis.crypto = {
|
||||
getRandomValues: (buffer) => {
|
||||
// Check if buffer is valid
|
||||
if (!buffer || typeof buffer.length !== 'number') {
|
||||
throw new Error("crypto.getRandomValues: invalid buffer");
|
||||
}
|
||||
// Fill with pseudo-random numbers
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
buffer[i] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
return buffer;
|
||||
},
|
||||
randomUUID: () => {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
};
|
||||
console.log("Polyfilled globalThis.crypto for Wizer/Spin");
|
||||
}
|
||||
51
server/routes/notifications.js
Normal file
51
server/routes/notifications.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Router } from 'express';
|
||||
import pool from '../db.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const router = Router();
|
||||
|
||||
// GET /api/notifications/:userId
|
||||
router.get('/:userId', async (req, res) => {
|
||||
try {
|
||||
const [rows] = await pool.query(
|
||||
'SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT 50',
|
||||
[req.params.userId]
|
||||
);
|
||||
res.json(rows);
|
||||
} catch (err) {
|
||||
console.error('Fetch notifications error:', err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/notifications/:id/read
|
||||
router.put('/:id/read', async (req, res) => {
|
||||
try {
|
||||
await pool.query('UPDATE notifications SET is_read = TRUE WHERE id = ?', [req.params.id]);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('Mark notification read error:', err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Helper: Create notification and emit if possible
|
||||
export async function createNotification(req, { userId, type, title, message, link }) {
|
||||
try {
|
||||
const id = randomUUID();
|
||||
await pool.query(
|
||||
'INSERT INTO notifications (id, user_id, type, title, message, link) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[id, userId, type, title, message, link]
|
||||
);
|
||||
|
||||
if (req.io) {
|
||||
req.io.to(userId).emit('notification', {
|
||||
id, user_id: userId, type, title, message, link, is_read: false, created_at: new Date()
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Create notification error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import pool from '../db.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { createNotification } from './notifications.js';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -88,6 +89,17 @@ router.post('/', async (req, res) => {
|
||||
[actId, id, '📝 Task created']);
|
||||
|
||||
await conn.commit();
|
||||
|
||||
if (assignee) {
|
||||
await createNotification(req, {
|
||||
userId: assignee,
|
||||
type: 'assignment',
|
||||
title: 'New Task Assigned',
|
||||
message: `You have been assigned to task: ${title}`,
|
||||
link: `/tasks?id=${id}`
|
||||
});
|
||||
}
|
||||
|
||||
const task = await getFullTask(id);
|
||||
res.status(201).json(task);
|
||||
} catch (err) {
|
||||
@@ -178,6 +190,24 @@ router.post('/:id/comments', async (req, res) => {
|
||||
const timestamp = new Date();
|
||||
await pool.query('INSERT INTO comments (id, task_id, user_id, text, timestamp) VALUES (?, ?, ?, ?, ?)',
|
||||
[id, req.params.id, userId, text, timestamp]);
|
||||
|
||||
// Mention detection: @[Name](userId)
|
||||
const mentions = text.match(/@\[([^\]]+)\]\(([^)]+)\)/g);
|
||||
if (mentions) {
|
||||
const mentionedUserIds = [...new Set(mentions.map(m => m.match(/\(([^)]+)\)/)[1]))];
|
||||
for (const mId of mentionedUserIds) {
|
||||
if (mId !== userId) {
|
||||
await createNotification(req, {
|
||||
userId: mId,
|
||||
type: 'mention',
|
||||
title: 'New Mention',
|
||||
message: `You were mentioned in a comment on task ${req.params.id}`,
|
||||
link: `/tasks?id=${req.params.id}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.status(201).json({ id, userId, text, timestamp: timestamp.toISOString() });
|
||||
} catch (err) {
|
||||
console.error('Add comment error:', err);
|
||||
|
||||
34
spin.toml
34
spin.toml
@@ -1,34 +0,0 @@
|
||||
spin_manifest_version = 2
|
||||
|
||||
[application]
|
||||
name = "scrum-manager"
|
||||
version = "1.0.0"
|
||||
authors = ["Antigravity <antigravity@example.com>"]
|
||||
description = "Scrum Manager application running on Fermyon Spin"
|
||||
|
||||
[[trigger.http]]
|
||||
route = "/api/..."
|
||||
component = "scrum-manager-api"
|
||||
|
||||
[[trigger.http]]
|
||||
route = "/..."
|
||||
component = "scrum-manager-ui"
|
||||
|
||||
[component.scrum-manager-api]
|
||||
source = "server/dist/main.wasm"
|
||||
allowed_outbound_hosts = ["mysql://localhost:3306", "https://*:*"]
|
||||
[component.scrum-manager-api.build]
|
||||
command = "npm run build:spin"
|
||||
workdir = "server"
|
||||
[component.scrum-manager-api.variables]
|
||||
db_host = "localhost"
|
||||
db_port = "3306"
|
||||
db_user = "root"
|
||||
db_password = "scrumpass"
|
||||
db_name = "scrum_manager"
|
||||
|
||||
[component.scrum-manager-ui]
|
||||
source = { url = "https://github.com/fermyon/spin-fileserver/releases/download/v0.1.0/spin_static_fs.wasm", digest = "sha256:96c76d9af86420b39eb6cd7be5550e3cb5d4cc4de572ce0fd1f6a29471536cb4" }
|
||||
files = [{ source = "dist", destination = "/" }]
|
||||
[component.scrum-manager-ui.build]
|
||||
command = "npm run build"
|
||||
101
src/App.tsx
101
src/App.tsx
@@ -12,6 +12,7 @@ import { TaskDrawer, AddTaskModal } from './TaskDrawer';
|
||||
import { DashboardPage } from './Dashboard';
|
||||
import { TeamTasksPage, MembersPage } from './Pages';
|
||||
import { ReportsPage } from './Reports';
|
||||
import { NotificationProvider } from './NotificationContext';
|
||||
import './index.css';
|
||||
|
||||
const PAGE_TITLES: Record<string, string> = {
|
||||
@@ -242,56 +243,58 @@ export default function App() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-shell">
|
||||
<TopNavbar title={pageTitle} filterUser={filterUser} onFilterChange={setFilterUser}
|
||||
searchQuery={searchQuery} onSearch={setSearchQuery} onNewTask={handleNewTask}
|
||||
onOpenSidebar={() => setSidebarOpen(true)} users={users} />
|
||||
<div className="app-body">
|
||||
<Sidebar currentUser={currentUser} activePage={activePage} onNavigate={handleNavigate}
|
||||
onSignOut={() => { setCurrentUser(null); setActivePage('calendar'); setActiveView('calendar'); setSidebarOpen(false); }}
|
||||
isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} users={users} />
|
||||
<div className="main-content">
|
||||
{displayPage === 'calendar' && (
|
||||
<CalendarView tasks={tasks} currentUser={currentUser} calMonth={calMonth} calView={calView}
|
||||
onMonthChange={setCalMonth} onViewChange={setCalView} onTaskClick={handleTaskClick}
|
||||
onDayClick={handleDayClick} filterUser={filterUser} searchQuery={searchQuery} users={users} />
|
||||
)}
|
||||
{displayPage === 'kanban' && (
|
||||
<KanbanBoard tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
||||
onAddTask={handleKanbanAdd} onMoveTask={handleMoveTask} filterUser={filterUser} searchQuery={searchQuery} users={users} />
|
||||
)}
|
||||
{displayPage === 'list' && (
|
||||
<ListView tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
||||
filterUser={filterUser} searchQuery={searchQuery} onToggleDone={handleToggleDone} users={users} />
|
||||
)}
|
||||
{displayPage === 'dashboard' && <DashboardPage tasks={tasks} currentUser={currentUser} users={users} />}
|
||||
{displayPage === 'mytasks' && (
|
||||
<ListView tasks={filteredMyTasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
||||
filterUser={null} searchQuery={searchQuery} onToggleDone={handleToggleDone} users={users} />
|
||||
)}
|
||||
{displayPage === 'teamtasks' && <TeamTasksPage tasks={tasks} currentUser={currentUser} users={users} />}
|
||||
{displayPage === 'reports' && <ReportsPage tasks={tasks} users={users} currentUser={currentUser} />}
|
||||
{displayPage === 'members' && <MembersPage tasks={tasks} users={users} currentUser={currentUser} onAddUser={handleAddUser} onDeleteUser={handleDeleteUser} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{VIEW_PAGES.includes(activePage) && (
|
||||
<BottomToggleBar activeView={activeView} onViewChange={handleViewChange} />
|
||||
)}
|
||||
|
||||
{activeTask && <TaskDrawer task={activeTask} currentUser={currentUser} onClose={() => setActiveTask(null)} onUpdate={handleUpdateTask} onAddDependency={handleAddDep} onToggleDependency={handleToggleDep} onRemoveDependency={handleRemoveDep} users={users} />}
|
||||
{showAddModal && <AddTaskModal onClose={() => setShowAddModal(false)} onAdd={handleAddTask} defaultDate={addModalDefaults.date} defaultStatus={addModalDefaults.status} users={users} currentUser={currentUser} />}
|
||||
|
||||
{quickAddDay && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 199 }} onClick={() => setQuickAddDay(null)}>
|
||||
<div style={{ position: 'absolute', top: Math.min(quickAddDay.rect.top, window.innerHeight - 280), left: Math.min(quickAddDay.rect.left, window.innerWidth - 340) }}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<QuickAddPanel date={quickAddDay.date} onAdd={handleQuickAdd}
|
||||
onOpenFull={() => { setAddModalDefaults({ date: quickAddDay.date }); setShowAddModal(true); setQuickAddDay(null); }}
|
||||
onClose={() => setQuickAddDay(null)} users={users} />
|
||||
<NotificationProvider userId={currentUser.id}>
|
||||
<div className="app-shell">
|
||||
<TopNavbar title={pageTitle} filterUser={filterUser} onFilterChange={setFilterUser}
|
||||
searchQuery={searchQuery} onSearch={setSearchQuery} onNewTask={handleNewTask}
|
||||
onOpenSidebar={() => setSidebarOpen(true)} users={users} />
|
||||
<div className="app-body">
|
||||
<Sidebar currentUser={currentUser} activePage={activePage} onNavigate={handleNavigate}
|
||||
onSignOut={() => { setCurrentUser(null); setActivePage('calendar'); setActiveView('calendar'); setSidebarOpen(false); }}
|
||||
isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} users={users} />
|
||||
<div className="main-content">
|
||||
{displayPage === 'calendar' && (
|
||||
<CalendarView tasks={tasks} currentUser={currentUser} calMonth={calMonth} calView={calView}
|
||||
onMonthChange={setCalMonth} onViewChange={setCalView} onTaskClick={handleTaskClick}
|
||||
onDayClick={handleDayClick} filterUser={filterUser} searchQuery={searchQuery} users={users} />
|
||||
)}
|
||||
{displayPage === 'kanban' && (
|
||||
<KanbanBoard tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
||||
onAddTask={handleKanbanAdd} onMoveTask={handleMoveTask} filterUser={filterUser} searchQuery={searchQuery} users={users} />
|
||||
)}
|
||||
{displayPage === 'list' && (
|
||||
<ListView tasks={tasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
||||
filterUser={filterUser} searchQuery={searchQuery} onToggleDone={handleToggleDone} users={users} />
|
||||
)}
|
||||
{displayPage === 'dashboard' && <DashboardPage tasks={tasks} currentUser={currentUser} users={users} />}
|
||||
{displayPage === 'mytasks' && (
|
||||
<ListView tasks={filteredMyTasks} currentUser={currentUser} onTaskClick={handleTaskClick}
|
||||
filterUser={null} searchQuery={searchQuery} onToggleDone={handleToggleDone} users={users} />
|
||||
)}
|
||||
{displayPage === 'teamtasks' && <TeamTasksPage tasks={tasks} currentUser={currentUser} users={users} />}
|
||||
{displayPage === 'reports' && <ReportsPage tasks={tasks} users={users} currentUser={currentUser} />}
|
||||
{displayPage === 'members' && <MembersPage tasks={tasks} users={users} currentUser={currentUser} onAddUser={handleAddUser} onDeleteUser={handleDeleteUser} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{VIEW_PAGES.includes(activePage) && (
|
||||
<BottomToggleBar activeView={activeView} onViewChange={handleViewChange} />
|
||||
)}
|
||||
|
||||
{activeTask && <TaskDrawer task={activeTask} currentUser={currentUser} onClose={() => setActiveTask(null)} onUpdate={handleUpdateTask} onAddDependency={handleAddDep} onToggleDependency={handleToggleDep} onRemoveDependency={handleRemoveDep} users={users} />}
|
||||
{showAddModal && <AddTaskModal onClose={() => setShowAddModal(false)} onAdd={handleAddTask} defaultDate={addModalDefaults.date} defaultStatus={addModalDefaults.status} users={users} currentUser={currentUser} />}
|
||||
|
||||
{quickAddDay && (
|
||||
<div style={{ position: 'fixed', inset: 0, zIndex: 199 }} onClick={() => setQuickAddDay(null)}>
|
||||
<div style={{ position: 'absolute', top: Math.min(quickAddDay.rect.top, window.innerHeight - 280), left: Math.min(quickAddDay.rect.left, window.innerWidth - 340) }}
|
||||
onClick={e => e.stopPropagation()}>
|
||||
<QuickAddPanel date={quickAddDay.date} onAdd={handleQuickAdd}
|
||||
onOpenFull={() => { setAddModalDefaults({ date: quickAddDay.date }); setShowAddModal(true); setQuickAddDay(null); }}
|
||||
onClose={() => setQuickAddDay(null)} users={users} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</NotificationProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { User } from './data';
|
||||
import { NotificationBell } from './components/NotificationBell';
|
||||
|
||||
interface TopNavbarProps {
|
||||
title: string;
|
||||
@@ -31,7 +32,7 @@ export function TopNavbar({ title, filterUser, onFilterChange, searchQuery, onSe
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button className="notif-btn">🔔<span className="notif-badge">3</span></button>
|
||||
<NotificationBell />
|
||||
<button className="new-task-btn" onClick={onNewTask}>+ New Task</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
80
src/NotificationContext.tsx
Normal file
80
src/NotificationContext.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { io } from 'socket.io-client';
|
||||
|
||||
export interface Notification {
|
||||
id: string;
|
||||
type: 'assignment' | 'mention' | 'update';
|
||||
title: string;
|
||||
message: string;
|
||||
link?: string;
|
||||
is_read: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface NotificationContextType {
|
||||
notifications: Notification[];
|
||||
unreadCount: number;
|
||||
markAsRead: (id: string) => Promise<void>;
|
||||
}
|
||||
|
||||
const NotificationContext = createContext<NotificationContextType | undefined>(undefined);
|
||||
|
||||
export const NotificationProvider: React.FC<{ children: React.ReactNode, userId: string }> = ({ children, userId }) => {
|
||||
const [notifications, setNotifications] = useState<Notification[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) return;
|
||||
|
||||
const newSocket = io(window.location.protocol + '//' + window.location.hostname + ':3001');
|
||||
|
||||
newSocket.emit('join', userId);
|
||||
|
||||
newSocket.on('notification', (notif: Notification) => {
|
||||
setNotifications(prev => [notif, ...prev]);
|
||||
// Optional: Show browser toast here
|
||||
});
|
||||
|
||||
fetchNotifications();
|
||||
|
||||
return () => {
|
||||
newSocket.close();
|
||||
};
|
||||
}, [userId]);
|
||||
|
||||
const fetchNotifications = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/notifications/${userId}`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setNotifications(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Fetch notifications failed', err);
|
||||
}
|
||||
};
|
||||
|
||||
const markAsRead = async (id: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/notifications/${id}/read`, { method: 'PUT' });
|
||||
if (res.ok) {
|
||||
setNotifications(prev => prev.map(n => n.id === id ? { ...n, is_read: true } : n));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Mark read failed', err);
|
||||
}
|
||||
};
|
||||
|
||||
const unreadCount = notifications.filter(n => !n.is_read).length;
|
||||
|
||||
return (
|
||||
<NotificationContext.Provider value={{ notifications, unreadCount, markAsRead }}>
|
||||
{children}
|
||||
</NotificationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useNotifications = () => {
|
||||
const context = useContext(NotificationContext);
|
||||
if (!context) throw new Error('useNotifications must be used within NotificationProvider');
|
||||
return context;
|
||||
};
|
||||
65
src/components/NotificationBell.tsx
Normal file
65
src/components/NotificationBell.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNotifications } from '../NotificationContext';
|
||||
|
||||
export const NotificationBell: React.FC = () => {
|
||||
const { notifications, unreadCount, markAsRead } = useNotifications();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<button
|
||||
className="p-2 rounded-full hover:bg-white/10 relative transition-colors"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<span className="text-xl">🔔</span>
|
||||
{unreadCount > 0 && (
|
||||
<span className="absolute top-1 right-1 bg-red-500 text-white text-[10px] font-bold px-1.5 py-0.5 rounded-full min-w-[18px] text-center border-2 border-[#0f172a]">
|
||||
{unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-2 w-80 bg-[#1e293b] border border-white/10 rounded-xl shadow-2xl z-50 overflow-hidden backdrop-blur-md">
|
||||
<div className="p-4 border-b border-white/10 flex justify-between items-center">
|
||||
<h3 className="font-semibold text-white">Notifications</h3>
|
||||
<span className="text-xs text-slate-400">{unreadCount} unread</span>
|
||||
</div>
|
||||
<div className="max-height-[400px] overflow-y-auto">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="p-8 text-center text-slate-500 italic">
|
||||
No notifications yet
|
||||
</div>
|
||||
) : (
|
||||
notifications.map(n => (
|
||||
<div
|
||||
key={n.id}
|
||||
className={`p-4 border-b border-white/5 hover:bg-white/5 cursor-pointer transition-colors ${!n.is_read ? 'bg-blue-500/5' : ''}`}
|
||||
onClick={() => markAsRead(n.id)}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<div className="text-lg">
|
||||
{n.type === 'assignment' ? '📋' : n.type === 'mention' ? '💬' : '🔔'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className={`text-sm ${!n.is_read ? 'text-white font-medium' : 'text-slate-300'}`}>
|
||||
{n.title}
|
||||
</p>
|
||||
<p className="text-xs text-slate-400 mt-1 truncate">
|
||||
{n.message}
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-500 mt-1">
|
||||
{new Date(n.created_at).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
{!n.is_read && <div className="w-2 h-2 bg-blue-500 rounded-full mt-2" />}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
5
temp_spin/temp/.gitignore
vendored
5
temp_spin/temp/.gitignore
vendored
@@ -1,5 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
target
|
||||
.spin/
|
||||
build/
|
||||
14
temp_spin/temp/.vscode/launch.json
vendored
14
temp_spin/temp/.vscode/launch.json
vendored
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "starlingmonkey",
|
||||
"request": "launch",
|
||||
"name": "Debug StarlingMonkey component",
|
||||
"component": "${workspaceFolder}/dist/temp.wasm",
|
||||
"program": "${workspaceFolder}/src/index.js",
|
||||
"stopOnEntry": false,
|
||||
"trace": true
|
||||
}
|
||||
]
|
||||
}
|
||||
12
temp_spin/temp/.vscode/settings.json
vendored
12
temp_spin/temp/.vscode/settings.json
vendored
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"starlingmonkey": {
|
||||
"componentRuntime": {
|
||||
"executable": "spin",
|
||||
"options": [
|
||||
"up",
|
||||
"-f",
|
||||
"${workspaceFolder}",
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
# `http-js` Template
|
||||
|
||||
A starter template for building JavaScript HTTP applications with Spin.
|
||||
|
||||
## Getting Started
|
||||
|
||||
Build the App
|
||||
|
||||
```bash
|
||||
spin build
|
||||
```
|
||||
|
||||
## Run the App
|
||||
|
||||
```bash
|
||||
spin up
|
||||
```
|
||||
|
||||
## Using Spin Interfaces
|
||||
|
||||
To use additional Spin interfaces, install the corresponding packages:
|
||||
|
||||
| Interface | Package |
|
||||
|---------------|---------------------------------|
|
||||
| Key-Value | `@spinframework/spin-kv` |
|
||||
| LLM | `@spinframework/spin-llm` |
|
||||
| MQTT | `@spinframework/spin-mqtt` |
|
||||
| MySQL | `@spinframework/spin-mysql` |
|
||||
| PostgreSQL | `@spinframework/spin-postgres` |
|
||||
| Redis | `@spinframework/spin-redis` |
|
||||
| SQLite | `@spinframework/spin-sqlite` |
|
||||
| Variables | `@spinframework/spin-variables` |
|
||||
|
||||
## Using the StarlingMonkey Debugger for VS Code
|
||||
|
||||
1. First install the [StarlingMonkey Debugger](https://marketplace.visualstudio.com/items?itemName=BytecodeAlliance.starlingmonkey-debugger) extension.
|
||||
2. Build the component using the debug command `npm run build:debug`.
|
||||
3. Uncomment `tcp://127.0.0.1:*` in the `allowed_outbound_hosts` field in the `spin.toml`.
|
||||
4. Start the debugger in VS Code which should start Spin and attach the debugger. The debugger needs to be restarted for each http call.
|
||||
@@ -1,42 +0,0 @@
|
||||
// build.mjs
|
||||
import { build } from 'esbuild';
|
||||
import path from 'path';
|
||||
import { SpinEsbuildPlugin } from "@spinframework/build-tools/plugins/esbuild/index.js";
|
||||
import fs from 'fs';
|
||||
|
||||
const spinPlugin = await SpinEsbuildPlugin();
|
||||
|
||||
// plugin to handle vendor files in node_modules that may not be bundled.
|
||||
// Instead of generating a real source map for these files, it appends a minimal
|
||||
// inline source map pointing to an empty source. This avoids errors and ensures
|
||||
// source maps exist even for unbundled vendor code.
|
||||
let SourceMapPlugin = {
|
||||
name: 'excludeVendorFromSourceMap',
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /node_modules/ }, args => {
|
||||
return {
|
||||
contents: fs.readFileSync(args.path, 'utf8')
|
||||
+ '\n//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIiJdLCJtYXBwaW5ncyI6IkEifQ==',
|
||||
loader: 'default',
|
||||
}
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
await build({
|
||||
entryPoints: ['./src/index.js'],
|
||||
outfile: './build/bundle.js',
|
||||
bundle: true,
|
||||
format: 'esm',
|
||||
platform: 'node',
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
plugins: [spinPlugin, SourceMapPlugin],
|
||||
logLevel: 'error',
|
||||
loader: {
|
||||
'.ts': 'ts',
|
||||
'.tsx': 'tsx',
|
||||
},
|
||||
resolveExtensions: ['.ts', '.tsx', '.js'],
|
||||
sourceRoot: path.resolve(process.cwd(), 'src'),
|
||||
});
|
||||
1710
temp_spin/temp/package-lock.json
generated
1710
temp_spin/temp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"name": "temp",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "node build.mjs && mkdirp dist && j2w -i build/bundle.js --initLocation http://temp.localhost -o dist/temp.wasm",
|
||||
"build:debug": "node build.mjs && mkdirp dist && j2w -d -i build/bundle.js --initLocation http://temp.localhost -o dist/temp.wasm",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"mkdirp": "^3.0.1",
|
||||
"esbuild": "^0.25.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"itty-router": "^5.0.18",
|
||||
"@spinframework/build-tools": "^1.0.4",
|
||||
"@spinframework/wasi-http-proxy": "^1.0.0"
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
spin_manifest_version = 2
|
||||
|
||||
[application]
|
||||
authors = ["tusuii <tusuii764@gmail.com>"]
|
||||
description = ""
|
||||
name = "temp"
|
||||
version = "0.1.0"
|
||||
|
||||
[[trigger.http]]
|
||||
route = "/..."
|
||||
component = "temp"
|
||||
|
||||
[component.temp]
|
||||
source = "dist/temp.wasm"
|
||||
exclude_files = ["**/node_modules"]
|
||||
allowed_outbound_hosts = [
|
||||
# "tcp://127.0.0.1:*", # Uncomment this line to while using the StarlingMonkey Debugger
|
||||
]
|
||||
[component.temp.build]
|
||||
command = ["npm install", "npm run build"]
|
||||
watch = ["src/**/*.js"]
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
// For AutoRouter documentation refer to https://itty.dev/itty-router/routers/autorouter
|
||||
import { AutoRouter } from 'itty-router';
|
||||
|
||||
let router = AutoRouter();
|
||||
|
||||
// Route ordering matters, the first route that matches will be used
|
||||
// Any route that does not return will be treated as a middleware
|
||||
// Any unmatched route will return a 404
|
||||
router
|
||||
.get('/', () => new Response('Hello, Spin!'))
|
||||
.get('/hello/:name', ({ name }) => `Hello, ${name}!`)
|
||||
|
||||
addEventListener('fetch', (event) => {
|
||||
event.respondWith(router.fetch(event.request));
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ export default defineConfig({
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://backend:3001',
|
||||
target: 'http://127.0.0.1:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user