Add phonebook
This commit is contained in:
parent
cd62b5bab8
commit
651a96a6d9
3 changed files with 327 additions and 0 deletions
|
|
@ -7,6 +7,7 @@
|
||||||
in {
|
in {
|
||||||
imports = [
|
imports = [
|
||||||
./accounts.nix
|
./accounts.nix
|
||||||
|
./extensions.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
sops.secrets."fieldpoc/omm" = {
|
sops.secrets."fieldpoc/omm" = {
|
||||||
|
|
|
||||||
47
hosts/pbx/services/fieldpoc/extensions.nix
Normal file
47
hosts/pbx/services/fieldpoc/extensions.nix
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
{pkgs, ...}: let
|
||||||
|
mkphonebook = pkgs.python3.pkgs.buildPythonPackage {
|
||||||
|
pname = "fieldpoc-mkphonebook";
|
||||||
|
version = "1.0";
|
||||||
|
|
||||||
|
src = ./mkphonebook.py;
|
||||||
|
|
||||||
|
dontUnpack = true;
|
||||||
|
|
||||||
|
propagatedBuildInputs = [pkgs.makeWrapper];
|
||||||
|
|
||||||
|
format = "other";
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/lib/mkphonebook
|
||||||
|
mkdir -p $out/bin
|
||||||
|
cp $src $out/lib/mkphonebook/script.py
|
||||||
|
|
||||||
|
makeWrapper ${pkgs.python3.interpreter} $out/bin/mkphonebook \
|
||||||
|
--add-flags "$out/lib/mkphonebook/script.py" \
|
||||||
|
--set PYTHONPATH "${pkgs.python3.pkgs.pyyaml}/${pkgs.python3.sitePackages}"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in {
|
||||||
|
environment.systemPackages = [
|
||||||
|
(
|
||||||
|
pkgs.writeShellScriptBin "fieldpoc-load-extensions" ''
|
||||||
|
set -e
|
||||||
|
|
||||||
|
tmpfile="$(mktemp -p /tmp tmp.extensions.XXXXXXXXXX.json)"
|
||||||
|
trap "rm -f $tmpfile" 0 2 3 15
|
||||||
|
|
||||||
|
${pkgs.yq}/bin/yq \
|
||||||
|
'.extensions[] |= with_entries(select(.key | IN("name", "type", "dialout_allowed", "trunk", "static_target", "callgroup_members", "sip_password", "dect_ipei")))' \
|
||||||
|
"$1" > $tmpfile
|
||||||
|
|
||||||
|
cat "$tmpfile" | /run/wrappers/bin/sudo -u fieldpoc tee /var/lib/fieldpoc/extensions.json >/dev/null
|
||||||
|
|
||||||
|
curl -s --fail --json '{}' http://127.0.0.1:9437/reload
|
||||||
|
|
||||||
|
${mkphonebook}/bin/mkphonebook "$1" "/persist/html/index.html"
|
||||||
|
|
||||||
|
rm -f $tmpfile
|
||||||
|
''
|
||||||
|
)
|
||||||
|
];
|
||||||
|
}
|
||||||
279
hosts/pbx/services/fieldpoc/mkphonebook.py
Normal file
279
hosts/pbx/services/fieldpoc/mkphonebook.py
Normal file
|
|
@ -0,0 +1,279 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
|
||||||
|
def generate_html(extensions):
|
||||||
|
# Icons per Typ
|
||||||
|
type_icons = {
|
||||||
|
"sip": "📱",
|
||||||
|
"dect": "📞",
|
||||||
|
"static": "📌",
|
||||||
|
"callgroup": "👥",
|
||||||
|
"temp": "⏳",
|
||||||
|
"default": "☎️",
|
||||||
|
}
|
||||||
|
|
||||||
|
def make_color(idx):
|
||||||
|
hue = (idx * 30 + idx % 2 * 180) % 360
|
||||||
|
return f"hsl({hue}, 50%, 95%)"
|
||||||
|
|
||||||
|
emergency_color = "hsl(0, 75%, 75%)"
|
||||||
|
|
||||||
|
# Generiere Farbzuordnung je Location
|
||||||
|
locations = sorted(set(info.get("location", "") for info in extensions.values()))
|
||||||
|
location_colors = {}
|
||||||
|
for idx, loc in enumerate(locations):
|
||||||
|
location_colors[loc] = make_color(idx + 1)
|
||||||
|
|
||||||
|
# Generiere HTML
|
||||||
|
html_header = """
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Telefonbuch</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
input[type="text"] {
|
||||||
|
width: 100%%;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--input-bg);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: var(--table-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
td.extension, td.vanity {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
td.extension {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
td > a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
td span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px 0px;
|
||||||
|
}
|
||||||
|
tr:hover {
|
||||||
|
background-color: var(--hover);
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background-color: var(--header-bg);
|
||||||
|
color: var(--header-fg);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.only-dialin .has-no-dialin {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Farbthemen */
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f5;
|
||||||
|
--fg: #000;
|
||||||
|
--border: #ccc;
|
||||||
|
--hover: #eee;
|
||||||
|
--header-bg: #4CAF50;
|
||||||
|
--header-fg: white;
|
||||||
|
--table-bg: white;
|
||||||
|
--input-bg: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #1e1e1e;
|
||||||
|
--fg: #f5f5f5;
|
||||||
|
--border: #444;
|
||||||
|
--hover: #333;
|
||||||
|
--header-bg: #2e7d32;
|
||||||
|
--header-fg: white;
|
||||||
|
--table-bg: #2a2a2a;
|
||||||
|
--input-bg: #2f2f2f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
body {
|
||||||
|
padding: 0px;
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 0px 4px;
|
||||||
|
}
|
||||||
|
td span {
|
||||||
|
padding: 6px 0px;
|
||||||
|
}
|
||||||
|
th.type, td.type {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
function searchTable() {
|
||||||
|
const input = document.getElementById("searchInput").value.toLowerCase();
|
||||||
|
const rows = document.querySelectorAll("tbody tr");
|
||||||
|
|
||||||
|
rows.forEach(row => {
|
||||||
|
const name = row.dataset.name.toLowerCase();
|
||||||
|
const ext = row.dataset.ext.toLowerCase();
|
||||||
|
const loc = row.dataset.location.toLowerCase();
|
||||||
|
const match = name.includes(input) || ext.includes(input) || loc.includes(input);
|
||||||
|
row.style.display = match ? "" : "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortTable(n) {
|
||||||
|
const table = document.getElementById("phoneTable");
|
||||||
|
let switching = true;
|
||||||
|
let dir = "asc";
|
||||||
|
let switchcount = 0;
|
||||||
|
|
||||||
|
while (switching) {
|
||||||
|
switching = false;
|
||||||
|
const rows = table.rows;
|
||||||
|
let tableCol = (row, col) => rows[row].querySelectorAll('td')[col].querySelector('span');
|
||||||
|
for (let i = 1; i < (rows.length - 1); i++) {
|
||||||
|
const x = tableCol(i, n);
|
||||||
|
const y = tableCol(i+1, n);
|
||||||
|
const cmp = x.textContent.trim().localeCompare(y.textContent.trim(), 'de', { numeric: true });
|
||||||
|
|
||||||
|
if ((dir === "asc" && cmp > 0) || (dir === "desc" && cmp < 0)) {
|
||||||
|
// Zeilen tauschen
|
||||||
|
rows[i].parentNode.insertBefore(rows[i + 1], rows[i]);
|
||||||
|
switching = true;
|
||||||
|
switchcount++;
|
||||||
|
break; // Nur ein Tausch pro Durchlauf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (switchcount === 0 && dir === "asc") {
|
||||||
|
dir = "desc";
|
||||||
|
switching = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterDialin(el) {
|
||||||
|
const table = document.getElementById("phoneTable");
|
||||||
|
if (el.checked) {
|
||||||
|
table.classList.add('only-dialin');
|
||||||
|
} else {
|
||||||
|
table.classList.remove('only-dialin');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", function(event) {
|
||||||
|
filterDialin(document.getElementById('filterDialin'));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>📖 Telefonbuch</h1>
|
||||||
|
<input type="text" id="searchInput" onkeyup="searchTable()" placeholder="Suche nach Name, Nummer oder Standort...">
|
||||||
|
<br>
|
||||||
|
<input type="checkbox" id="filterDialin" onchange="filterDialin(this)">
|
||||||
|
<label for="filterDialin">Nur Nummern mit externer Einwahl anzeigen</label>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<table id="phoneTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th onclick="sortTable(5)"></th>
|
||||||
|
<th class="extension" onclick="sortTable(1)"></th>
|
||||||
|
<th onclick="sortTable(2)">🧑 Name</th>
|
||||||
|
<th onclick="sortTable(3)">🔤 </th>
|
||||||
|
<th onclick="sortTable(4)">📍 Ort</th>
|
||||||
|
<th class="type" onclick="sortTable(5)">🔧 Typ</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Sortiere Einträge:
|
||||||
|
# 1. Notrufnummern (3-Stellig) numerisch sortiert
|
||||||
|
# 2. alle weiteren Extensions: nach Location, dann Name
|
||||||
|
rows = sorted(
|
||||||
|
extensions.items(),
|
||||||
|
key=lambda x: (
|
||||||
|
(0, x[0], "")
|
||||||
|
if len(x[0]) < 4
|
||||||
|
else (1, x[1].get("location", ""), x[1].get("name", ""))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
html_rows = ""
|
||||||
|
|
||||||
|
for ext, info in rows:
|
||||||
|
name = info.get("name", "")
|
||||||
|
typ = info.get("type", "default")
|
||||||
|
location = info.get("location", "")
|
||||||
|
vanity = info.get("vanity", "")
|
||||||
|
dialin = info.get("dialin", "")
|
||||||
|
icon = type_icons.get(typ, type_icons["default"])
|
||||||
|
row_color = (
|
||||||
|
emergency_color if len(ext) < 4 else location_colors.get(location, "#fff")
|
||||||
|
)
|
||||||
|
|
||||||
|
name_cell = f"<span>{name}</span>"
|
||||||
|
if dialin:
|
||||||
|
name_cell = f'<a href="tel:{dialin}">{name_cell}</a>'
|
||||||
|
|
||||||
|
html_rows += f"""
|
||||||
|
<tr class="{'has-dialin' if dialin else 'has-no-dialin'}" data-name="{name}" data-ext="{ext}" data-location="{location}" style="background-color: {row_color}">
|
||||||
|
<td><span>{icon}</span></td>
|
||||||
|
<td class="extension"><a href="tel:{ext}"><span>{ext}</span></a></td>
|
||||||
|
<td>{name_cell}</td>
|
||||||
|
<td class="vanity"><span>{vanity}</span></td>
|
||||||
|
<td class="location"><span>{location}</span></td>
|
||||||
|
<td class="type"><span>{typ}</span></td>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
|
||||||
|
html_footer = """
|
||||||
|
</tbody>
|
||||||
|
</div>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
return html_header + html_rows + html_footer
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
INPUT_FILE, OUTPUT_FILE = sys.argv[1:]
|
||||||
|
|
||||||
|
with open(INPUT_FILE, "r") as f:
|
||||||
|
data = yaml.safe_load(f)
|
||||||
|
|
||||||
|
extensions = data.get("extensions", {})
|
||||||
|
with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
|
||||||
|
f.write(generate_html(extensions))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue