Compare commits

...

6 Commits

Author SHA1 Message Date
KS Jannette
b3a08e8f25 css trick
Some checks are pending
check / check (push) Waiting to run
2026-03-02 00:28:13 -05:00
KS Jannette
8716be0201 fudge 2026-03-02 00:09:24 -05:00
KS Jannette
647be874e5 m 2026-03-02 00:09:01 -05:00
KS Jannette
c12c4fe742 add image 2026-03-01 23:14:25 -05:00
S Jannette
5f5253d495 Revise README for clarity and feature description
Updated the README to clarify features and improve readability.
2026-03-01 21:48:53 -05:00
S Jannette
bbe2374703 Merge pull request #11 from kjannette/history-reconfig-possible-bugfix
History reconfig possible bugfix
2026-03-01 20:38:56 -05:00
5 changed files with 192 additions and 142 deletions

View File

@@ -5,7 +5,6 @@ A lightweight on-chain monitoring and alerting system designed to give users sit
Koin Ping observes on-chain activity and notifies users when predefined conditions are met. It does not execute transactions, manage wallets, or speculate on prices.
## Getting Started
### Prerequisites
@@ -46,16 +45,16 @@ cd frontend && npm run dev
```
The API listens on `http://localhost:3001` and the frontend on
`http://localhost:3000` by default.
`http://localhost:3000` by default for development.
## Rationale
Crypto users who hold or actively monitor addresses need a lightweight, reliable
way to know when on-chain activity occurs without polling block explorers
manually. Koin Ping fills that gap: it watches a set of Ethereum addresses,
evaluates configurable alert rules (incoming transactions, outgoing
transactions, large transfers, balance thresholds), and notifies the user
through Discord webhooks.
Crypto users may actively monitor addresses with webhooks integrating popular
messaging platforms: Discord, Slack, Telegram.
This lightweight, reliable framework makes instant awareness of on-chain
activity trivial, without polling block explorers manually. Koin Ping watches
addresses, evaluates configurable alert rules (incoming transactions, outgoing
transactions, "large" transfers, balance thresholds), and sends notifications.
## Design
@@ -132,7 +131,7 @@ To receive alerts via Telegram, you need to create a bot and get your chat ID.
4. In the JSON response, find the `"chat"` object — the `"id"` field is your **Chat ID** (a numeric value).
> **Tip:** If the `"result"` array is empty, make sure you sent a message to your bot first, then refresh the page.
> **Tip:** If the `'result"` array is empty, make sure you sent a message to your bot first, then refresh the page.
#### 3. Save in Koin Ping
@@ -153,3 +152,5 @@ MIT. See [LICENSE](LICENSE).
## Author
Steven Jannette

BIN
frontend/public/ping.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -26,8 +26,8 @@ export default function Navbar() {
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
<span style={{
fontWeight: 700,
fontSize: "1.1rem",
color: "#fff",
fontSize: "1.4rem",
color: "#e62525",
marginRight: "2rem",
letterSpacing: "0.5px",
}}>

View File

@@ -0,0 +1,88 @@
.login-page {
min-height: 100vh;
background-image: url(/ping.png);
background-size: 67%;
background-position: center;
background-repeat: no-repeat;
}
.login-card {
max-width: 400px;
margin: 20px auto;
padding: 2rem;
padding-top: 8rem;
border: 1px solid #333;
border-radius: 8px;
background-color: rgba(0, 0, 0, 0.75);
}
.login-heading {
margin-bottom: 2rem;
text-align: center;
}
.login-brand {
color: #e62525;
}
.login-error {
padding: 0.75rem;
margin-bottom: 1rem;
background-color: #ff000020;
border: 1px solid #ff0000;
border-radius: 4px;
color: #ff6666;
}
.login-field {
margin-bottom: 1rem;
}
.login-field-last {
margin-bottom: 1.5rem;
}
.login-label {
display: block;
margin-bottom: 0.5rem;
}
.login-input {
width: 100%;
padding: 0.5rem;
font-size: 1.2rem;
background-color: #242424;
border: 1px solid #444;
border-radius: 4px;
color: white;
}
.login-button {
width: 100%;
padding: 0.75rem;
font-size: 1.2rem;
background-color: #0066cc;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.login-button:disabled {
background-color: #333;
cursor: not-allowed;
}
.login-footer {
margin-top: 1.5rem;
text-align: center;
}
.login-footer-text {
color: #b3b3b3;
}
.login-signup-link {
color: #0066cc;
text-decoration: none;
}

View File

@@ -1,146 +1,107 @@
/**
* Login Page
*
* Allows existing users to sign in with email and password
*/
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useAuth } from "../contexts/AuthContext";
import "./Login.css";
export default function Login() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [isVisible, setIsVisible] = useState(false);
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const { login } = useAuth();
const navigate = useNavigate();
const { login } = useAuth();
const navigate = useNavigate();
async function handleSubmit(e) {
e.preventDefault();
async function handleSubmit(e) {
e.preventDefault();
// Validation
if (!email || !password) {
setError("Please fill in all fields");
return;
}
try {
setError("");
setLoading(true);
await login(email, password);
navigate("/addresses"); // Redirect to main app
} catch (err) {
setError("Failed to log in: " + err.message);
} finally {
setLoading(false);
}
// Validation
if (!email || !password) {
setError("Please fill in all fields");
return;
}
return (
<div
style={{
maxWidth: "400px",
margin: "4rem auto",
padding: "2rem",
border: "1px solid #333",
borderRadius: "8px",
}}
>
<h1 style={{ marginBottom: "2rem", textAlign: "center" }}>
Koin Ping - Login
</h1>
try {
setError("");
setLoading(true);
await login(email, password);
navigate("/addresses"); // Redirect to main app
} catch (err) {
setError("Failed to log in: " + err.message);
} finally {
setLoading(false);
}
}
{error && (
<div
style={{
padding: "0.75rem",
marginBottom: "1rem",
backgroundColor: "#ff000020",
border: "1px solid #ff0000",
borderRadius: "4px",
color: "#ff6666",
}}
>
{error}
</div>
)}
return (
<div className="login-page">
<div
className="login-card"
onMouseEnter={() => setIsVisible(true)}
>
<h1 className="login-heading">
<span className="login-brand">Koin Ping</span> - Login
</h1>
<form onSubmit={handleSubmit}>
<div style={{ marginBottom: "1rem" }}>
<label style={{ display: "block", marginBottom: "0.5rem" }}>
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
style={{
width: "100%",
padding: "0.5rem",
fontSize: "1rem",
backgroundColor: "#242424",
border: "1px solid #444",
borderRadius: "4px",
color: "white",
}}
required
/>
</div>
<div style={{ marginBottom: "1.5rem" }}>
<label style={{ display: "block", marginBottom: "0.5rem" }}>
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
style={{
width: "100%",
padding: "0.5rem",
fontSize: "1rem",
backgroundColor: "#242424",
border: "1px solid #444",
borderRadius: "4px",
color: "white",
}}
required
/>
</div>
<button
type="submit"
disabled={loading}
style={{
width: "100%",
padding: "0.75rem",
fontSize: "1rem",
backgroundColor: loading ? "#333" : "#0066cc",
color: "white",
border: "none",
borderRadius: "4px",
cursor: loading ? "not-allowed" : "pointer",
}}
>
{loading ? "Logging in..." : "Log In"}
</button>
</form>
<div style={{ marginTop: "1.5rem", textAlign: "center" }}>
<p style={{ color: "#b3b3b3" }}>
Don't have an account?{" "}
<Link
to="/signup"
style={{ color: "#0066cc", textDecoration: "none" }}
>
Sign up here
</Link>
</p>
<div style={{ visibility: isVisible ? 'visible' : 'hidden' }}>
{error && (
<div className="login-error">
{error}
</div>
)}
<form onSubmit={handleSubmit}>
<div className="login-field">
<label className="login-label">
Email
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
disabled={loading}
className="login-input"
required
/>
</div>
<div className="login-field-last">
<label className="login-label">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
disabled={loading}
className="login-input"
required
/>
</div>
<button
type="submit"
disabled={loading}
className="login-button"
>
{loading ? "Logging in..." : "Log In"}
</button>
</form>
<div className="login-footer">
<p className="login-footer-text">
Don't have an account?{" "}
<Link
to="/signup"
className="login-signup-link"
>
Sign up here
</Link>
</p>
</div>
</div>
);
</div>
</div>
);
}