Compare commits

...

21 Commits

Author SHA1 Message Date
Mateusz Gruszczyński
46d986ce80 zmiany w js i html 2025-08-31 11:21:58 +02:00
gru
ec07782444 Update static/css/main.css 2025-08-31 10:44:04 +02:00
gru
69adc272ff Update static/css/main.css 2025-08-30 18:18:29 +02:00
gru
68e45a7477 Update static/css/main.css 2025-08-30 18:16:15 +02:00
gru
da4f9907ae Update static/css/main.css 2025-08-30 18:15:10 +02:00
gru
fec9d9c5dd Update static/css/main.css 2025-08-30 18:11:46 +02:00
gru
6c58d7f524 Update static/css/main.css 2025-08-30 18:11:01 +02:00
gru
ae98702806 Update static/css/main.css 2025-08-30 18:10:24 +02:00
gru
163df6233b Update static/css/main.css 2025-08-30 18:08:55 +02:00
gru
78cf869354 Update static/css/main.css 2025-08-30 18:06:54 +02:00
gru
79b7ea77f4 Update app.py 2025-08-30 17:59:25 +02:00
gru
6347a96db8 Update app.py 2025-08-30 09:54:53 +02:00
gru
94d5130470 Merge pull request 'refactor' (#1) from refactor into master
Reviewed-on: #1
2025-08-30 00:19:49 +02:00
gru
aaea5cdeef Update app.py 2025-08-29 23:57:39 +02:00
gru
a2febd82c1 Update templates/form.html 2025-08-29 23:55:32 +02:00
gru
e065f5892d Update app.py 2025-08-29 23:49:32 +02:00
gru
1afaea2d00 Update app.py 2025-08-29 23:47:57 +02:00
gru
809b1168e1 Update templates/form.html 2025-08-29 23:45:23 +02:00
Mateusz Gruszczyński
259d716e0f poprawki 2025-08-29 22:36:41 +02:00
Mateusz Gruszczyński
43c9a7006c poprawki 2025-08-29 22:32:22 +02:00
Mateusz Gruszczyński
0419ca1d9d poprawki 2025-08-29 22:24:36 +02:00
4 changed files with 104 additions and 37 deletions

59
app.py
View File

@@ -6,6 +6,7 @@ import time
import json
import hashlib
import ipaddress
import hmac, ipaddress
from datetime import datetime
from urllib.parse import urlparse, quote, unquote, urljoin
from functools import wraps
@@ -94,8 +95,6 @@ def finalize_response(response):
if response.status_code >= 400:
response.headers["Cache-Control"] = "no-store"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
if path.startswith("/static/"):
@@ -108,8 +107,6 @@ def finalize_response(response):
if path == "/":
response.headers["Cache-Control"] = "private, no-store"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
return response
@@ -279,16 +276,34 @@ def track_url_request(url):
def add_recent_link(url, target_ip):
ts = datetime.now().isoformat()
link_data = f"{ts}|{url}|{target_ip}"
new_item = f"{ts}|{url}|{target_ip}"
key = "recent_links"
current = redis_client.lrange(key, 0, -1)
filtered = []
for raw in current:
try:
s = raw.decode()
parts = s.split("|")
if len(parts) >= 3 and parts[1] == url and parts[2] == target_ip:
continue
except Exception:
pass
filtered.append(raw)
with redis_client.pipeline() as pipe:
pipe.lpush("recent_links", link_data)
pipe.ltrim("recent_links", 0, 9)
pipe.delete(key)
pipe.lpush(key, new_item)
if filtered:
pipe.rpush(key, *filtered[:99])
pipe.ltrim(key, 0, 99)
pipe.execute()
redis_client.incr("stats:recent_links_added")
def get_recent_links():
links = redis_client.lrange("recent_links", 0, 9)
links = redis_client.lrange("recent_links", 0, 99)
out = []
for link in links:
parts = link.decode().split("|")
@@ -344,31 +359,11 @@ def favicon():
@app.route("/", methods=["GET"])
def index():
generated_link = None
recent_links = get_recent_links()
url_param = request.args.get("url", config.DEFAULT_SOURCE_URL)
raw_ip = request.args.get("ip", "127.0.0.1")
try:
target_ip = validate_ip(raw_ip)
except ValueError:
target_ip = "127.0.0.1"
if url_param:
try:
normalized = validate_and_normalize_url(unquote(url_param))
encoded = quote(normalized, safe="")
generated_link = urljoin(
request.host_url, f"convert?url={encoded}&ip={target_ip}"
)
add_recent_link(normalized, target_ip)
recent_links = get_recent_links()
except Exception as e:
app.logger.error(f"Error processing URL: {str(e)}")
try:
return render_template(
"form.html",
generated_link=generated_link,
recent_links=recent_links,
client_ip=get_client_ip(),
user_agent=request.headers.get("User-Agent", "Unknown"),
@@ -376,7 +371,6 @@ def index():
except Exception:
return jsonify(
{
"generated_link": generated_link,
"recent_links": recent_links,
"client_ip": get_client_ip(),
"user_agent": request.headers.get("User-Agent", "Unknown"),
@@ -387,7 +381,7 @@ def index():
@app.route("/convert")
@limiter.limit(config.RATE_LIMIT_CONVERT)
def convert():
import hmac, ipaddress
def is_private_client_ip() -> bool:
ip = get_client_ip()
@@ -436,8 +430,8 @@ def convert():
return resp
try:
redis_client.incr("stats:convert_requests")
add_recent_convert()
redis_client.incr("stats:convert_requests")
if debug_mode:
d("Start /convert w trybie debug")
@@ -515,11 +509,13 @@ def convert():
if debug_mode:
d("Upstream 304 zwracam 304")
r.close()
add_recent_link(normalized_url, target_ip)
return debug_response(status=304)
resp = Response(status=304)
resp.headers.update(cache_headers(etag, r.headers.get("Last-Modified")))
resp.direct_passthrough = True
r.close()
add_recent_link(normalized_url, target_ip)
return resp
up_etag = r.headers.get("ETag")
@@ -566,6 +562,7 @@ def convert():
resp.headers.update(cache_headers(etag, up_lm))
resp.direct_passthrough = True
redis_client.incr("stats:conversions_success")
add_recent_link(normalized_url, target_ip)
return resp
except requests.exceptions.RequestException as e:

View File

@@ -194,7 +194,8 @@ select:focus {
.form-actions {
display: flex;
gap: 10px;
align-items: center
align-items: center;
justify-content: flex-end;
}
/* Result */
@@ -324,14 +325,20 @@ select:focus {
color: #fff;
box-shadow: 0 10px 20px color-mix(in srgb, var(--brand) 35%, transparent);
transition: transform .04s ease, filter .15s ease, box-shadow .15s ease;
display: inline-block;
text-decoration: none;
display: inline-block;
}
.btn:hover {
filter: brightness(1.05)
transition: transform 0.15s ease;
display: inline-block;
transform: scale(1.05);
}
.btn:active {
transform: translateY(1px)
transform: scale(0.95);
}
.btn.outline {
@@ -357,6 +364,30 @@ select:focus {
border-radius: 14px
}
a.btn:hover {
text-decoration: none;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
a.btn[aria-disabled="true"] {
opacity: .5;
cursor: not-allowed;
pointer-events: none;
filter: none;
transform: none;
}
a.btn[aria-disabled="true"]:hover {
filter: none;
transform: none;
text-decoration: none;
}
/* Toast */
#toast {
position: fixed;
@@ -661,6 +692,16 @@ select:focus {
background: var(--bg-elev)
}
.result-box .hint {
display: block;
text-align: center;
font-size: .9rem;
color: var(--muted);
opacity: 0.5;
opacity: 1.1;
text-shadow: 0 0 4px rgba(123, 212, 255, 0.4);
}
#error-dump {
margin: 0;
padding: 12px;
@@ -673,6 +714,7 @@ select:focus {
overflow-wrap: anywhere;
}
@media (max-width:720px) {
.error-card {
padding: 12px
@@ -685,4 +727,18 @@ select:focus {
#error-dump {
max-height: 300px
}
}
.result-box {
margin-top: 14px;
padding: 12px;
border: 1px dashed var(--border);
border-radius: 12px;
background: var(--bg-elev);
box-shadow: 0 0 8px 2px color-mix(in srgb, var(--brand) 50%, transparent 50%);
transition: box-shadow 0.3s ease;
}
.result-box:hover {
box-shadow: 0 0 12px 4px color-mix(in srgb, var(--brand) 70%, transparent 30%);
}

View File

@@ -86,6 +86,7 @@
const ipPreset = $('#ip-preset');
const out = $('#generated-link');
const openBtn = $('#open-link');
const copyBtn = $('#copy-btn');
function showError(input, msg) {
const id = input.getAttribute('id');
@@ -94,6 +95,7 @@
input.setAttribute('aria-invalid', msg ? 'true' : 'false');
}
function updatePreview() {
const rawUrl = (urlInput?.value || '').trim();
const ip = (ipInput?.value || '').trim();
@@ -103,12 +105,14 @@
if (openBtn) {
openBtn.setAttribute('href', '#');
openBtn.setAttribute('aria-disabled', 'true');
openBtn.setAttribute('disabled', 'true');
}
if (copyBtn) copyBtn.setAttribute('disabled', 'true');
$('.result-box')?.setAttribute('data-state', 'empty');
return;
}
const normalized = normalizeUrlMaybe(rawUrl); // poprawny http/https lub ''
const normalized = normalizeUrlMaybe(rawUrl);
const guessed = rawUrl ? (rawUrl.includes('://') ? rawUrl : `https://${rawUrl}`) : '';
const previewUrl = normalized || guessed;
@@ -117,7 +121,9 @@
if (openBtn) {
openBtn.setAttribute('href', '#');
openBtn.setAttribute('aria-disabled', 'true');
openBtn.setAttribute('disabled', 'true');
}
if (copyBtn) copyBtn.setAttribute('disabled', 'true');
$('.result-box')?.setAttribute('data-state', 'empty');
return;
}
@@ -130,11 +136,14 @@
if (ok) {
openBtn.setAttribute('href', link);
openBtn.setAttribute('aria-disabled', 'false');
openBtn.removeAttribute('disabled');
} else {
openBtn.setAttribute('href', '#');
openBtn.setAttribute('aria-disabled', 'true');
openBtn.setAttribute('disabled', 'true');
}
}
if (copyBtn) copyBtn.toggleAttribute('disabled', !ok);
$('.result-box')?.setAttribute('data-state', ok ? 'ready' : 'empty');
}

View File

@@ -83,12 +83,17 @@
<input id="generated-link" type="text" value="{{ generated_link or '' }}" readonly
placeholder="Link will appear here…">
<div class="result-buttons">
<button class="btn" type="button" data-action="copy" data-target="#generated-link">Copy</button>
<button class="btn" type="button" id="copy-btn" data-action="copy"
data-target="#generated-link">Copy</button>
<a class="btn outline" id="open-link" href="{{ generated_link or '#' }}" target="_blank"
rel="noopener" aria-disabled="{{ 'false' if generated_link else 'true' }}">Open</a>
</div>
</div>
<small class="hint">The preview updates live while you type.</small>
<small class="hint">Paste this link in your Mikrotik (IP -> DNS -> Adlist) or other DNS server /
ad blocking tool</small>
</div>
</section>