Initial commit.
This commit is contained in:
commit
73096b465d
181
.gitignore
vendored
Normal file
181
.gitignore
vendored
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# UV
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
#uv.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
# Ruff stuff:
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# PyPI configuration file
|
||||||
|
.pypirc
|
||||||
|
|
||||||
|
# Cursor
|
||||||
|
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||||
|
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||||
|
# refer to https://docs.cursor.com/context/ignore-files
|
||||||
|
.cursorignore
|
||||||
|
.cursorindexingignore
|
||||||
28
app.py
Normal file
28
app.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# app.py
|
||||||
|
from flask import Flask, render_template, url_for
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
@app.route("/chmod")
|
||||||
|
def chmod():
|
||||||
|
return render_template("chmod.html")
|
||||||
|
|
||||||
|
@app.route("/cron")
|
||||||
|
def cron():
|
||||||
|
return render_template("cron.html")
|
||||||
|
|
||||||
|
@app.route("/json")
|
||||||
|
def json():
|
||||||
|
return render_template("json.html")
|
||||||
|
|
||||||
|
@app.route("/yaml")
|
||||||
|
def yaml():
|
||||||
|
return render_template("yaml.html")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True)
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
blinker==1.9.0
|
||||||
|
click==8.2.1
|
||||||
|
Flask==3.1.1
|
||||||
|
itsdangerous==2.2.0
|
||||||
|
Jinja2==3.1.6
|
||||||
|
MarkupSafe==3.0.2
|
||||||
|
Werkzeug==3.1.3
|
||||||
142
templates/chmod.html
Normal file
142
templates/chmod.html
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>KPR chmod - Calculadora de permisos</title>
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 50%;
|
||||||
|
margin: 2rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row label {
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bits {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<label for="octal">Octal:</label>
|
||||||
|
<input id="octal" type="number" min="0" max="777" step="1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>Owner:</label>
|
||||||
|
<div class="bits">
|
||||||
|
<label><input type="checkbox" id="owner-read" /> r</label>
|
||||||
|
<label><input type="checkbox" id="owner-write" /> w</label>
|
||||||
|
<label><input type="checkbox" id="owner-execute" /> x</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>Group:</label>
|
||||||
|
<div class="bits">
|
||||||
|
<label><input type="checkbox" id="group-read" /> r</label>
|
||||||
|
<label><input type="checkbox" id="group-write" /> w</label>
|
||||||
|
<label><input type="checkbox" id="group-execute" /> x</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<label>Others:</label>
|
||||||
|
<div class="bits">
|
||||||
|
<label><input type="checkbox" id="others-read" /> r</label>
|
||||||
|
<label><input type="checkbox" id="others-write" /> w</label>
|
||||||
|
<label><input type="checkbox" id="others-execute" /> x</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// grab references
|
||||||
|
const octalInput = document.getElementById('octal');
|
||||||
|
const bits = {
|
||||||
|
owner: {
|
||||||
|
read: document.getElementById('owner-read'),
|
||||||
|
write: document.getElementById('owner-write'),
|
||||||
|
execute: document.getElementById('owner-execute'),
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
read: document.getElementById('group-read'),
|
||||||
|
write: document.getElementById('group-write'),
|
||||||
|
execute: document.getElementById('group-execute'),
|
||||||
|
},
|
||||||
|
others: {
|
||||||
|
read: document.getElementById('others-read'),
|
||||||
|
write: document.getElementById('others-write'),
|
||||||
|
execute: document.getElementById('others-execute'),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// helper: from digit 0–7 set the three checkboxes
|
||||||
|
function setBitsFromDigit(digit, checkboxes) {
|
||||||
|
checkboxes.read.checked = !!(digit & 4);
|
||||||
|
checkboxes.write.checked = !!(digit & 2);
|
||||||
|
checkboxes.execute.checked = !!(digit & 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper: read three checkboxes to produce 0–7
|
||||||
|
function getDigitFromBits(checkboxes) {
|
||||||
|
return (checkboxes.read.checked ? 4 : 0)
|
||||||
|
+ (checkboxes.write.checked ? 2 : 0)
|
||||||
|
+ (checkboxes.execute.checked ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// when octal input changes, update all checkboxes
|
||||||
|
function updateCheckboxesFromOctal() {
|
||||||
|
let v = octalInput.value.toString();
|
||||||
|
// pad or trim to exactly 3 digits
|
||||||
|
v = v.padStart(3, '0').slice(-3);
|
||||||
|
const ownerDigit = parseInt(v[0], 10) || 0;
|
||||||
|
const groupDigit = parseInt(v[1], 10) || 0;
|
||||||
|
const othersDigit = parseInt(v[2], 10) || 0;
|
||||||
|
|
||||||
|
setBitsFromDigit(ownerDigit, bits.owner);
|
||||||
|
setBitsFromDigit(groupDigit, bits.group);
|
||||||
|
setBitsFromDigit(othersDigit, bits.others);
|
||||||
|
}
|
||||||
|
|
||||||
|
// when any checkbox changes, recompute octal
|
||||||
|
function updateOctalFromCheckboxes() {
|
||||||
|
const owner = getDigitFromBits(bits.owner);
|
||||||
|
const group = getDigitFromBits(bits.group);
|
||||||
|
const others = getDigitFromBits(bits.others);
|
||||||
|
octalInput.value = `${owner}${group}${others}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wire up events
|
||||||
|
octalInput.addEventListener('input', updateCheckboxesFromOctal);
|
||||||
|
|
||||||
|
for (const scope of Object.values(bits)) {
|
||||||
|
for (const cb of Object.values(scope)) {
|
||||||
|
cb.addEventListener('change', updateOctalFromCheckboxes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize on load
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// default to 644
|
||||||
|
octalInput.value = '644';
|
||||||
|
updateCheckboxesFromOctal();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
212
templates/cron.html
Normal file
212
templates/cron.html
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Cronjob Syntax Helper</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
gap: 2rem;
|
||||||
|
width: 60%;
|
||||||
|
/* margin: auto; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.left,
|
||||||
|
.right {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row {
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row label {
|
||||||
|
width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-row input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left .field-row input {
|
||||||
|
/* fix inputs to ~6rem (≈96px) wide */
|
||||||
|
flex: 0 0 6rem;
|
||||||
|
width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#cron-desc {
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: #f2f2f2;
|
||||||
|
border-radius: 4px;
|
||||||
|
min-height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
width: 60%;
|
||||||
|
margin: 1.5rem auto 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom select,
|
||||||
|
.bottom button {
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Left column: build fields -->
|
||||||
|
<div class="left">
|
||||||
|
<div class="field-row">
|
||||||
|
<label for="minute">Minute</label>
|
||||||
|
<input type="text" id="minute" placeholder="*" />
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<label for="hour">Hour</label>
|
||||||
|
<input type="text" id="hour" placeholder="*" />
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<label for="dom">Day of Mo.</label>
|
||||||
|
<input type="text" id="dom" placeholder="*" />
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<label for="month">Month</label>
|
||||||
|
<input type="text" id="month" placeholder="*" />
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<label for="dow">Day of Wk.</label>
|
||||||
|
<input type="text" id="dow" placeholder="*" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right column: output -->
|
||||||
|
<div class="right">
|
||||||
|
<div class="field-row">
|
||||||
|
<label for="cron-expr">Cron Expr</label>
|
||||||
|
<input type="text" id="cron-expr" readonly />
|
||||||
|
</div>
|
||||||
|
<div class="field-row">
|
||||||
|
<label>Description</label>
|
||||||
|
<div id="cron-desc"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Presets + Actions -->
|
||||||
|
<div class="bottom">
|
||||||
|
<div>
|
||||||
|
Presets:
|
||||||
|
<select id="preset">
|
||||||
|
<option value="">— choose —</option>
|
||||||
|
<option value="* * * * *">Every minute</option>
|
||||||
|
<option value="0 * * * *">Every hour</option>
|
||||||
|
<option value="0 0 * * *">Daily at midnight</option>
|
||||||
|
<option value="0 0 * * 1">Weekly Mon @ midnight</option>
|
||||||
|
<option value="0 0 1 * *">Monthly 1st @ midnight</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="copy-btn">Copy cron expr</button>
|
||||||
|
<button id="reset-btn">Reset</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// grab elements
|
||||||
|
const minuteIn = document.getElementById('minute');
|
||||||
|
const hourIn = document.getElementById('hour');
|
||||||
|
const domIn = document.getElementById('dom');
|
||||||
|
const monthIn = document.getElementById('month');
|
||||||
|
const dowIn = document.getElementById('dow');
|
||||||
|
const cronExpr = document.getElementById('cron-expr');
|
||||||
|
const cronDesc = document.getElementById('cron-desc');
|
||||||
|
const preset = document.getElementById('preset');
|
||||||
|
const copyBtn = document.getElementById('copy-btn');
|
||||||
|
const resetBtn = document.getElementById('reset-btn');
|
||||||
|
|
||||||
|
// build the cron string
|
||||||
|
function updateCron() {
|
||||||
|
const parts = [
|
||||||
|
minuteIn.value.trim() || '*',
|
||||||
|
hourIn.value.trim() || '*',
|
||||||
|
domIn.value.trim() || '*',
|
||||||
|
monthIn.value.trim() || '*',
|
||||||
|
dowIn.value.trim() || '*'
|
||||||
|
];
|
||||||
|
cronExpr.value = parts.join(' ');
|
||||||
|
updateDesc(parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// basic plain-English breakdown
|
||||||
|
function updateDesc([min, hr, dom, mon, dow]) {
|
||||||
|
if ([min, hr, dom, mon, dow].every(v => v === '*')) {
|
||||||
|
cronDesc.textContent = 'Every minute';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parts = [];
|
||||||
|
if (min !== '*') parts.push(`minute = ${min}`);
|
||||||
|
if (hr !== '*') parts.push(`hour = ${hr}`);
|
||||||
|
if (dom !== '*') parts.push(`day of month = ${dom}`);
|
||||||
|
if (mon !== '*') parts.push(`month = ${mon}`);
|
||||||
|
if (dow !== '*') parts.push(`day of week = ${dow}`);
|
||||||
|
cronDesc.textContent = parts.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// wire inputs
|
||||||
|
[minuteIn, hourIn, domIn, monthIn, dowIn].forEach(el => {
|
||||||
|
el.addEventListener('input', () => {
|
||||||
|
// clear preset selection when typing freeform
|
||||||
|
preset.value = '';
|
||||||
|
updateCron();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// presets
|
||||||
|
preset.addEventListener('change', () => {
|
||||||
|
const v = preset.value;
|
||||||
|
if (!v) return;
|
||||||
|
const p = v.split(' ');
|
||||||
|
[minuteIn.value, hourIn.value, domIn.value, monthIn.value, dowIn.value] = p;
|
||||||
|
updateCron();
|
||||||
|
});
|
||||||
|
|
||||||
|
// copy button
|
||||||
|
copyBtn.addEventListener('click', () => {
|
||||||
|
navigator.clipboard.writeText(cronExpr.value)
|
||||||
|
.then(() => alert('Copied to clipboard!'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// reset button
|
||||||
|
resetBtn.addEventListener('click', () => {
|
||||||
|
minuteIn.value = hourIn.value = domIn.value = monthIn.value = dowIn.value = '*';
|
||||||
|
preset.value = '';
|
||||||
|
updateCron();
|
||||||
|
});
|
||||||
|
|
||||||
|
// init
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
resetBtn.click(); // sets everything to "* * * * *"
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
85
templates/index.html
Normal file
85
templates/index.html
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>Helpers Home</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-container {
|
||||||
|
display: grid;
|
||||||
|
/* each column will be at least 360px, but can grow to fill the space */
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
width: 95%;
|
||||||
|
max-width: 1600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* extra height so tall tools fit without internal scrollbars */
|
||||||
|
.panel {
|
||||||
|
min-height: 360px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Let wide panels span two columns on wide screens */
|
||||||
|
.panel.wide {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel iframe {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.panel h2 {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background: #f5f5f5;
|
||||||
|
font-size: 1rem;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel iframe {
|
||||||
|
flex: 1;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>KPR Utils</h1>
|
||||||
|
<div class="panel-container">
|
||||||
|
<div class="panel">
|
||||||
|
<h2>Permissions Calculator</h2>
|
||||||
|
<iframe src="{{ url_for('chmod') }}" title="chmod helper"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel wide">
|
||||||
|
<h2>Cron Syntax Helper</h2>
|
||||||
|
<iframe src="{{ url_for('cron') }}" title="cron helper"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel wide">
|
||||||
|
<h2>JSON Validator</h2>
|
||||||
|
<iframe src="{{ url_for('json') }}" title="json helper"></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="panel wide">
|
||||||
|
<h2>YAML Validator</h2>
|
||||||
|
<iframe src="{{ url_for('yaml') }}" title="yaml helper"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
135
templates/json.html
Normal file
135
templates/json.html
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>JSON Formatter & Validator</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane textarea {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #c00;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
min-height: 1.2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Input pane -->
|
||||||
|
<div class="pane">
|
||||||
|
<label for="input-json">Input JSON</label>
|
||||||
|
<textarea id="input-json" placeholder="Paste or type JSON here…"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Output pane -->
|
||||||
|
<div class="pane">
|
||||||
|
<label for="output-json">Formatted JSON</label>
|
||||||
|
<textarea id="output-json" readonly placeholder="Valid JSON will appear here…"></textarea>
|
||||||
|
<div id="error" class="error"></div>
|
||||||
|
<div class="controls">
|
||||||
|
<button id="copy-btn">Copy Formatted</button>
|
||||||
|
<button id="clear-btn">Clear All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const inTA = document.getElementById('input-json');
|
||||||
|
const outTA = document.getElementById('output-json');
|
||||||
|
const errDiv = document.getElementById('error');
|
||||||
|
const copyBtn = document.getElementById('copy-btn');
|
||||||
|
const clearBtn = document.getElementById('clear-btn');
|
||||||
|
|
||||||
|
function formatJSON() {
|
||||||
|
const text = inTA.value.trim();
|
||||||
|
if (!text) {
|
||||||
|
outTA.value = '';
|
||||||
|
errDiv.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(text);
|
||||||
|
outTA.value = JSON.stringify(obj, null, 2);
|
||||||
|
errDiv.textContent = '';
|
||||||
|
} catch (e) {
|
||||||
|
outTA.value = '';
|
||||||
|
errDiv.textContent = '❌ ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live update on input
|
||||||
|
inTA.addEventListener('input', formatJSON);
|
||||||
|
|
||||||
|
// Copy formatted JSON
|
||||||
|
copyBtn.addEventListener('click', () => {
|
||||||
|
if (!outTA.value) return;
|
||||||
|
navigator.clipboard.writeText(outTA.value)
|
||||||
|
.then(() => alert('Formatted JSON copied!'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear both panes
|
||||||
|
clearBtn.addEventListener('click', () => {
|
||||||
|
inTA.value = '';
|
||||||
|
outTA.value = '';
|
||||||
|
errDiv.textContent = '';
|
||||||
|
inTA.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
clearBtn.click();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
138
templates/yaml.html
Normal file
138
templates/yaml.html
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>YAML Formatter & Validator</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
width: 80%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pane textarea {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls button {
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: #c00;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
min-height: 1.2em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<!-- Input pane -->
|
||||||
|
<div class="pane">
|
||||||
|
<label for="input-yaml">Input YAML</label>
|
||||||
|
<textarea id="input-yaml" placeholder="Paste or type YAML here…"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Output pane -->
|
||||||
|
<div class="pane">
|
||||||
|
<label for="output-yaml">Formatted YAML</label>
|
||||||
|
<textarea id="output-yaml" readonly placeholder="Valid YAML will appear here…"></textarea>
|
||||||
|
<div id="error-yaml" class="error"></div>
|
||||||
|
<div class="controls">
|
||||||
|
<button id="copy-yaml">Copy Formatted</button>
|
||||||
|
<button id="clear-yaml">Clear All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- js-yaml from CDN -->
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
|
||||||
|
<script>
|
||||||
|
const inTA = document.getElementById('input-yaml');
|
||||||
|
const outTA = document.getElementById('output-yaml');
|
||||||
|
const errDiv = document.getElementById('error-yaml');
|
||||||
|
const copyBtn = document.getElementById('copy-yaml');
|
||||||
|
const clearBtn = document.getElementById('clear-yaml');
|
||||||
|
|
||||||
|
function formatYAML() {
|
||||||
|
const text = inTA.value.trim();
|
||||||
|
if (!text) {
|
||||||
|
outTA.value = '';
|
||||||
|
errDiv.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// parse then dump with 2-space indent
|
||||||
|
const obj = jsyaml.load(text);
|
||||||
|
outTA.value = jsyaml.dump(obj, { indent: 2 });
|
||||||
|
errDiv.textContent = '';
|
||||||
|
} catch (e) {
|
||||||
|
outTA.value = '';
|
||||||
|
errDiv.textContent = '❌ ' + e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Live update on input
|
||||||
|
inTA.addEventListener('input', formatYAML);
|
||||||
|
|
||||||
|
// Copy formatted YAML
|
||||||
|
copyBtn.addEventListener('click', () => {
|
||||||
|
if (!outTA.value) return;
|
||||||
|
navigator.clipboard.writeText(outTA.value)
|
||||||
|
.then(() => alert('Formatted YAML copied!'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear both panes
|
||||||
|
clearBtn.addEventListener('click', () => {
|
||||||
|
inTA.value = '';
|
||||||
|
outTA.value = '';
|
||||||
|
errDiv.textContent = '';
|
||||||
|
inTA.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
clearBtn.click();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue
Block a user