1 Commits
main ... test

Author SHA1 Message Date
c43f6c0845 test.csv hinzugefügt 2025-11-05 11:53:15 +00:00
3 changed files with 211 additions and 307 deletions

View File

@@ -1,51 +0,0 @@
name: release-tag
on:
push:
branches:
- 'main'
jobs:
release-image:
runs-on: ubuntu-fast
env:
DOCKER_ORG: ${{ vars.DOCKER_ORG }}
DOCKER_LATEST: latest
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v2
with: # replace it with your local IP
config-inline: |
[registry."${{ vars.DOCKER_REGISTRY }}"]
http = true
insecure = true
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: ${{ vars.DOCKER_REGISTRY }} # replace it with your local IP
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
push: true
tags: | # replace it with your local IP and tags
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
${{ vars.DOCKER_REGISTRY }}/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}

355
main.go
View File

@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"flag" "flag"
"fmt"
"html/template" "html/template"
"log" "log"
"net/http" "net/http"
@@ -22,14 +21,10 @@ var (
csvPath = flag.String("csv", "data.csv", "path to CSV with header row") csvPath = flag.String("csv", "data.csv", "path to CSV with header row")
keyCol = flag.String("key", "", "column name to search in (default: first column)") keyCol = flag.String("key", "", "column name to search in (default: first column)")
valueCol = flag.String("value", "", "column name to return (default: second column)") valueCol = flag.String("value", "", "column name to return (default: second column)")
suffixLen = flag.Int("suffix", 0, "if >0, also match by last N characters of the scanned code")
sep = flag.String("sep", ",", "CSV field separator (e.g. ',' or ';')")
records []record records []record
headers []string headers []string
mu sync.RWMutex mu sync.RWMutex
exactMap map[string]record // canon(key) -> record
suffixMap map[string][]record // lastN(canon(key), N) -> records
) )
func main() { func main() {
@@ -38,22 +33,24 @@ func main() {
if err := loadCSV(*csvPath); err != nil { if err := loadCSV(*csvPath); err != nil {
log.Fatalf("CSV laden fehlgeschlagen: %v", err) log.Fatalf("CSV laden fehlgeschlagen: %v", err)
} }
log.Printf("CSV geladen: %s (%d Zeilen, %d Spalten) | key=%q value=%q sep=%q suffix=%d", log.Printf("CSV geladen: %s (%d Zeilen, %d Spalten)", *csvPath, len(records), len(headers))
*csvPath, len(records), len(headers), *keyCol, *valueCol, *sep, *suffixLen)
http.HandleFunc("/", handleIndex) http.HandleFunc("/", handleIndex)
http.HandleFunc("/search", handleSearch) http.HandleFunc("/search", handleSearch)
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir()))))
fmt.Println(headers)
fmt.Println(records)
log.Printf("Starte Server auf %s …", *addr) log.Printf("Starte Server auf %s …", *addr)
if err := http.ListenAndServe(*addr, nil); err != nil { if err := http.ListenAndServe(*addr, nil); err != nil {
log.Fatal(err) log.Fatal(err)
} }
} }
func errUnknownCol(kind, name string, headers []string) error { func staticDir() string {
return errors.New(kind + " column '" + name + "' nicht gefunden. Verfügbare Spalten: " + strings.Join(headers, ", ")) dir := "static"
if _, err := os.Stat(dir); err == nil {
return dir
}
return "."
} }
func loadCSV(path string) error { func loadCSV(path string) error {
@@ -64,11 +61,6 @@ func loadCSV(path string) error {
defer f.Close() defer f.Close()
r := csv.NewReader(f) r := csv.NewReader(f)
if *sep != "" {
r.Comma = []rune(*sep)[0]
} else {
r.Comma = ','
}
r.FieldsPerRecord = -1 r.FieldsPerRecord = -1
rows, err := r.ReadAll() rows, err := r.ReadAll()
@@ -80,27 +72,25 @@ func loadCSV(path string) error {
} }
headers = rows[0] headers = rows[0]
idxKey := 0
idxVal := 1
if len(headers) < 2 { if len(headers) < 2 {
return errors.New("mindestens zwei Spalten benötigt") return errors.New("mindestens zwei Spalten benötigt")
} }
// Spaltenindizes bestimmen (EqualFold)
idxKey := 0
idxVal := 1
if *keyCol != "" { if *keyCol != "" {
if i := indexOf(headers, *keyCol); i >= 0 { idxKey = indexOf(headers, *keyCol)
idxKey = i if idxKey == -1 {
} else {
return errUnknownCol("key", *keyCol, headers) return errUnknownCol("key", *keyCol, headers)
} }
} }
if *valueCol != "" { if *valueCol != "" {
if i := indexOf(headers, *valueCol); i >= 0 { idxVal = indexOf(headers, *valueCol)
idxVal = i if idxVal == -1 {
} else {
return errUnknownCol("value", *valueCol, headers) return errUnknownCol("value", *valueCol, headers)
} }
} }
if *keyCol == "" { if *keyCol == "" {
*keyCol = headers[idxKey] *keyCol = headers[idxKey]
} }
@@ -108,7 +98,6 @@ func loadCSV(path string) error {
*valueCol = headers[idxVal] *valueCol = headers[idxVal]
} }
// Zeilen einlesen + trimmen
var recs []record var recs []record
for i := 1; i < len(rows); i++ { for i := 1; i < len(rows); i++ {
row := rows[i] row := rows[i]
@@ -119,162 +108,23 @@ func loadCSV(path string) error {
} }
m := make(record, len(headers)) m := make(record, len(headers))
for c, h := range headers { for c, h := range headers {
val := ""
if c < len(row) { if c < len(row) {
val = strings.TrimSpace(row[c]) m[h] = row[c]
} else {
m[h] = ""
} }
m[h] = val
} }
recs = append(recs, m) recs = append(recs, m)
} }
// Indizes bauen
ex := make(map[string]record, len(recs))
var sx map[string][]record
if *suffixLen > 0 {
sx = make(map[string][]record, len(recs))
}
for _, rec := range recs {
k := rec[*keyCol]
if k == "" {
continue
}
ck := canon(k)
ex[ck] = rec
if *suffixLen > 0 {
suf := lastN(ck, *suffixLen)
if suf != "" {
sx[suf] = append(sx[suf], rec)
}
}
}
mu.Lock() mu.Lock()
records = recs records = recs
exactMap = ex
suffixMap = sx
mu.Unlock() mu.Unlock()
// kleine Sichtprobe für Debug
log.Printf("Header: %v", headers)
if len(recs) > 0 {
log.Printf("Sample Key raw=%q canon=%q", recs[0][*keyCol], canon(recs[0][*keyCol]))
}
return nil return nil
} }
func handleIndex(w http.ResponseWriter, r *http.Request) { func errUnknownCol(kind, name string, headers []string) error {
tpl := template.Must(template.New("index").Parse(indexHTML)) return errors.New(kind + " column '" + name + "' nicht gefunden. Verfügbare Spalten: " + strings.Join(headers, ", "))
data := struct {
File string
KeyCol string
ValCol string
Suffix int
HasSuf bool
}{
File: filepath.Base(*csvPath),
KeyCol: *keyCol,
ValCol: *valueCol,
Suffix: *suffixLen,
HasSuf: *suffixLen > 0,
}
_ = tpl.Execute(w, data)
}
func handleSearch(w http.ResponseWriter, r *http.Request) {
q := strings.TrimSpace(r.URL.Query().Get("q"))
qc := canon(q) // WICHTIG: normalisieren
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if q == "" {
_ = json.NewEncoder(w).Encode(searchResp{
Query: q,
Message: "Gib einen Barcode ein.",
Headers: headers,
SuffixLen: *suffixLen,
SearchKeyCol: *keyCol,
ReturnValueCol: *valueCol,
})
return
}
mu.RLock()
defer mu.RUnlock()
resp := searchResp{
Query: q,
Headers: headers,
SuffixLen: *suffixLen,
SearchKeyCol: *keyCol,
ReturnValueCol: *valueCol,
}
// 1) Exakter Treffer (case/whitespace-insensitive)
if rec, ok := exactMap[qc]; ok {
resp.ExactHit = &item{
Key: rec[*keyCol],
Value: rec[*valueCol],
FullRow: rec,
}
_ = json.NewEncoder(w).Encode(resp)
return
}
// 2) Suffix-Treffer (letzte N Zeichen) mit canon
if *suffixLen > 0 && len(qc) >= *suffixLen {
suf := lastN(qc, *suffixLen)
if cands, ok := suffixMap[suf]; ok && len(cands) > 0 {
resp.SuffixUsed = true
resp.TopCandidate = &item{
Key: cands[0][*keyCol],
Value: cands[0][*valueCol],
FullRow: cands[0],
}
if len(cands) > 1 {
limit := len(cands)
if limit > 10 {
limit = 10
}
resp.Candidates = toItems(cands[1:limit])
}
_ = json.NewEncoder(w).Encode(resp)
return
}
}
// 3) Teiltreffer (Substring) in der Key-Spalte
var partial []item
for _, rec := range records {
if strings.Contains(canon(rec[*keyCol]), qc) {
partial = append(partial, item{
Key: rec[*keyCol],
Value: rec[*valueCol],
FullRow: rec,
})
if len(partial) >= 20 {
break
}
}
}
if len(partial) > 0 {
resp.TopCandidate = &partial[0]
if len(partial) > 1 {
if len(partial) > 10 {
resp.Candidates = partial[1:10]
} else {
resp.Candidates = partial[1:]
}
}
} else {
resp.Message = "Kein Treffer."
}
_ = json.NewEncoder(w).Encode(resp)
}
func canon(s string) string {
return strings.ToLower(strings.TrimSpace(s))
} }
func indexOf(ss []string, s string) int { func indexOf(ss []string, s string) int {
@@ -286,28 +136,13 @@ func indexOf(ss []string, s string) int {
return -1 return -1
} }
func lastN(s string, n int) string {
if n <= 0 {
return ""
}
r := []rune(s)
if len(r) <= n {
return s
}
return string(r[len(r)-n:])
}
type searchResp struct { type searchResp struct {
Query string `json:"query"` Query string `json:"query"`
ExactHit *item `json:"exact_hit,omitempty"` ExactHit *item `json:"exact_hit,omitempty"`
SuffixUsed bool `json:"suffix_used,omitempty"`
TopCandidate *item `json:"top_candidate,omitempty"` TopCandidate *item `json:"top_candidate,omitempty"`
Candidates []item `json:"candidates,omitempty"` Candidates []item `json:"candidates,omitempty"`
Message string `json:"message,omitempty"` Message string `json:"message,omitempty"`
Headers []string `json:"headers,omitempty"` Headers []string `json:"headers,omitempty"`
SuffixLen int `json:"suffix_len,omitempty"`
SearchKeyCol string `json:"search_key_col,omitempty"`
ReturnValueCol string `json:"return_value_col,omitempty"`
} }
type item struct { type item struct {
@@ -316,16 +151,71 @@ type item struct {
FullRow record `json:"full_row,omitempty"` FullRow record `json:"full_row,omitempty"`
} }
func toItems(rs []record) []item { func handleIndex(w http.ResponseWriter, r *http.Request) {
out := make([]item, 0, len(rs)) tpl := template.Must(template.New("index").Parse(indexHTML))
for _, rec := range rs { data := struct {
out = append(out, item{ File string
Key: rec[*keyCol], KeyCol string
ValCol string
}{
File: filepath.Base(*csvPath),
KeyCol: *keyCol,
ValCol: *valueCol,
}
_ = tpl.Execute(w, data)
}
func handleSearch(w http.ResponseWriter, r *http.Request) {
q := strings.TrimSpace(r.URL.Query().Get("q"))
w.Header().Set("Content-Type", "application/json; charset=utf-8")
if q == "" {
_ = json.NewEncoder(w).Encode(searchResp{Query: q, Message: "Gib einen Suchbegriff ein.", Headers: headers})
return
}
mu.RLock()
defer mu.RUnlock()
lq := strings.ToLower(q)
var exact *item
var partial []item
for _, rec := range records {
k := rec[*keyCol]
if strings.EqualFold(k, q) {
exact = &item{
Key: k,
Value: rec[*valueCol],
FullRow: rec,
}
break
}
if strings.Contains(strings.ToLower(k), lq) {
partial = append(partial, item{
Key: k,
Value: rec[*valueCol], Value: rec[*valueCol],
FullRow: rec, FullRow: rec,
}) })
} }
return out }
resp := searchResp{Query: q, Headers: headers}
if exact != nil {
resp.ExactHit = exact
} else if len(partial) > 0 {
resp.TopCandidate = &partial[0]
if len(partial) > 1 {
limit := len(partial)
if limit > 10 {
limit = 10
}
resp.Candidates = partial[:limit]
}
} else {
resp.Message = "Kein Treffer."
}
_ = json.NewEncoder(w).Encode(resp)
} }
const indexHTML = ` const indexHTML = `
@@ -337,8 +227,8 @@ const indexHTML = `
<title>CSV-Suche</title> <title>CSV-Suche</title>
<style> <style>
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 2rem; } body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; margin: 2rem; }
.wrap { max-width: 880px; margin: 0 auto; } .wrap { max-width: 840px; margin: 0 auto; }
input[type="text"] { width: 100%; font-size: 1.35rem; padding: .8rem 1rem; box-sizing: border-box; } input[type="text"] { width: 100%; font-size: 1.25rem; padding: .75rem 1rem; box-sizing: border-box; }
.muted { color: #666; font-size: .95rem; margin-top: .5rem; } .muted { color: #666; font-size: .95rem; margin-top: .5rem; }
.result { margin-top: 1rem; padding: 1rem; border: 1px solid #ddd; border-radius: .5rem; } .result { margin-top: 1rem; padding: 1rem; border: 1px solid #ddd; border-radius: .5rem; }
.hit { font-weight: 600; margin-bottom: .5rem; } .hit { font-weight: 600; margin-bottom: .5rem; }
@@ -347,17 +237,16 @@ const indexHTML = `
.row { padding: .25rem 0; border-bottom: 1px dashed #eee; } .row { padding: .25rem 0; border-bottom: 1px dashed #eee; }
code { background: #f6f8fa; padding: .15rem .35rem; border-radius: .25rem; } code { background: #f6f8fa; padding: .15rem .35rem; border-radius: .25rem; }
.pill { display: inline-block; padding: .1rem .5rem; border: 1px solid #ddd; border-radius: 999px; margin-right: .25rem; font-size: .85rem; } .pill { display: inline-block; padding: .1rem .5rem; border: 1px solid #ddd; border-radius: 999px; margin-right: .25rem; font-size: .85rem; }
.green { color: #0a7d32; }
</style> </style>
</head> </head>
<body> <body>
<div class="wrap"> <div class="wrap">
<h1>Barcode-Suche</h1> <h1>CSV-Suche</h1>
<p class="muted"> <p class="muted">
Datei: <code>{{ .File }}</code> • Suche in <span class="pill">{{ .KeyCol }}</span> → zeige <span class="pill">{{ .ValCol }}</span>{{if .HasSuf}} • Suffix: <span class="pill">{{ .Suffix }}</span>{{end}} Datei: <code>{{ .File }}</code> • Suche in <span class="pill">{{ .KeyCol }}</span> → zeige <span class="pill">{{ .ValCol }}</span>
</p> </p>
<input id="q" type="text" placeholder="Barcode scannen …" autocomplete="off" autofocus /> <input id="q" type="text" placeholder="Suchtext eingeben …" autocomplete="off" autofocus />
<div id="result" class="result" aria-live="polite"></div> <div id="result" class="result" aria-live="polite"></div>
</div> </div>
@@ -367,75 +256,66 @@ const indexHTML = `
var qEl = document.getElementById('q'); var qEl = document.getElementById('q');
var resEl = document.getElementById('result'); var resEl = document.getElementById('result');
// Fokus halten & Starttext // Fokus immer halten
window.addEventListener('load', function() { window.addEventListener('load', function() { qEl.focus(); qEl.select(); });
qEl.focus(); qEl.select();
resEl.innerHTML = '<span class="muted">Gib einen Barcode ein und bestätige mit Enter.</span>';
});
window.addEventListener('click', function() { qEl.focus(); }); window.addEventListener('click', function() { qEl.focus(); });
// NUR Enter löst eine Suche aus var timer = null;
qEl.addEventListener('input', function() {
clearTimeout(timer);
timer = setTimeout(runSearch, 120);
});
qEl.addEventListener('keydown', function(e) { qEl.addEventListener('keydown', function(e) {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
var submitted = qEl.value.trim(); runSearch(true);
if (!submitted) { qEl.focus(); return; }
runSearch(true, submitted, true); // highlight=true, clearAfter=true
} }
}); });
/** function runSearch(highlight) {
* runSearch(highlight, qOverride, clearAfter) var q = qEl.value.trim();
* - highlight: bool (nur als Flag nutzbar, falls du später Styling willst)
* - qOverride: string | null, wenn gesetzt wird dieser Wert verwendet
* - clearAfter: bool, Eingabefeld nach dem Request leeren
*/
function runSearch(highlight, qOverride, clearAfter) {
var q = (qOverride !== null && qOverride !== undefined) ? qOverride : qEl.value.trim();
if (!q) { if (!q) {
resEl.innerHTML = '<span class="muted">Gib einen Barcode ein und bestätige mit Enter.</span>'; resEl.innerHTML = '<span class="muted">Gib einen Suchbegriff ein.</span>';
return; return;
} }
fetch('/search?q=' + encodeURIComponent(q)) fetch('/search?q=' + encodeURIComponent(q))
.then(function(r) { return r.json(); }) .then(function(r) { return r.json(); })
.then(function(d) { render(d, !!highlight); }) .then(function(data) {
.catch(function() { resEl.innerHTML = '<span class="muted">Fehler bei der Suche.</span>'; }) render(data, !!highlight);
.finally(function() {
if (clearAfter) { qEl.value = ''; }
qEl.focus(); qEl.focus();
try {
var len = qEl.value.length; var len = qEl.value.length;
qEl.setSelectionRange(len, len); try { qEl.setSelectionRange(len, len); } catch(e) {}
} catch(e) {} })
.catch(function() {
resEl.innerHTML = '<span class="muted">Fehler bei der Suche.</span>';
}); });
} }
function render(d, highlight) { function render(d, highlight) {
if (d.exact_hit) { if (d.ExactHit) {
resEl.innerHTML = resEl.innerHTML =
'<div class="hit">Exakter Treffer für <code>' + escapeHtml(d.query) + '</code>:</div>' + '<div class="hit">Exakter Treffer für <code>' + escapeHtml(d.Query) + '</code>:</div>' +
'<div class="value"><h2>' + escapeHtml(d.exact_hit.value || '') + '</h2></div>' + '<div class="value">' + escapeHtml(d.ExactHit.Value || '') + '</div>' +
renderRow(d.exact_hit.full_row, d.headers); renderRow(d.ExactHit.FullRow, d.Headers);
return; return;
} }
if (d.top_candidate) { if (d.TopCandidate) {
var badge = d.suffix_used ? ' <span class="pill green">Suffix-Match (' + (d.suffix_len||'') + ')</span>' : '';
var list = ''; var list = '';
if (d.candidates && d.candidates.length > 0) { if (d.Candidates && d.Candidates.length > 0) {
list = '<div class="list"><div class="muted">Weitere Kandidaten:</div>' + list = '<div class="list"><div class="muted">Weitere Kandidaten:</div>' +
d.candidates.map(function(c) { d.Candidates.map(function(c) {
return '<div class="row"><code>' + escapeHtml(c.key) + '</code> → ' + escapeHtml(c.value || '') + '</div>'; return '<div class="row"><code>' + escapeHtml(c.Key) + '</code> → ' + escapeHtml(c.Value || '') + '</div>';
}).join('') + }).join('') +
'</div>'; '</div>';
} }
resEl.innerHTML = resEl.innerHTML =
'<div class="hit">Bester Treffer zu <code>' + escapeHtml(d.query) + '</code>:' + badge + '</div>' + '<div class="hit">Bester Treffer zu <code>' + escapeHtml(d.Query) + '</code>:</div>' +
'<div class="value"><h2>' + escapeHtml(d.top_candidate.value || '') + '</h2></div>' + '<div class="value">' + escapeHtml(d.TopCandidate.Value || '') + '</div>' +
renderRow(d.top_candidate.full_row, d.headers) + renderRow(d.TopCandidate.FullRow, d.Headers) +
list; list;
return; return;
} }
resEl.innerHTML = '<span class="muted">' + (d.message || 'Kein Treffer.') + '</span>'; resEl.innerHTML = '<span class="muted">' + (d.Message || 'Kein Treffer.') + '</span>';
} }
function renderRow(row, headers) { function renderRow(row, headers) {
@@ -451,10 +331,11 @@ const indexHTML = `
return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;', "'":'&#039;'}[m]); return ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;', "'":'&#039;'}[m]);
}); });
} }
// initial
runSearch(false);
})(); })();
</script> </script>
</body> </body>
</html> </html>
` `

74
test.csv Normal file
View File

@@ -0,0 +1,74 @@
HardwareTyp;TYP;Seriennummer;PC
Desktop;Lenovo ThinkCentre M75q Gen 5;GM0Z35GF;PC1001
Desktop;Lenovo ThinkCentre M75q Gen 5;GM0Z35BR;PC1002
Desktop;Lenovo ThinkCentre M75q Gen 5;GM0Z35JT;PC1003
Desktop;Lenovo ThinkCentre M75q Gen 5;GM0Z35JY;PC1004
Desktop;Lenovo ThinkCentre M75q Gen 5;GM0Z35BZ;PC1005
Desktop;Lenovo ThinkCentre M75q Gen 5;GM0Z35CA;PC1006
Desktop;Lenovo ThinkCentre M75q Gen 5;GM0Z35BY;PC1007
Desktop;Lenovo ThinkCentre M75q Gen 5;GM0Z35JW;PC1008
Desktop;Lenovo ThinkCentre M75q Gen 5;GM0Z35JZ;PC1009
Desktop;Lenovo ThinkCentre M75q Gen 5;GM0Z35CB;PC1010
0;0;0;0
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9M;NB1201
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9R;NB1202
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9V;NB1203
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9W;NB1204
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9X;NB1205
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4LA2;NB1206
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9K;NB1207
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9P;NB1208
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9Q;NB1209
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9S;NB1210
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4LA0;NB1211
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9G;NB1212
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9H;NB1213
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9N;NB1214
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9J;NB1215
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9F;NB1216
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4LA1;NB1217
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9Y;NB1218
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9L;NB1219
Laptop;Lenovo ThinkPad L14 Gen 6;PW0L4L9T;NB1220
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
0;0;0;0
1 HardwareTyp TYP Seriennummer PC
2 Desktop Lenovo ThinkCentre M75q Gen 5 GM0Z35GF PC1001
3 Desktop Lenovo ThinkCentre M75q Gen 5 GM0Z35BR PC1002
4 Desktop Lenovo ThinkCentre M75q Gen 5 GM0Z35JT PC1003
5 Desktop Lenovo ThinkCentre M75q Gen 5 GM0Z35JY PC1004
6 Desktop Lenovo ThinkCentre M75q Gen 5 GM0Z35BZ PC1005
7 Desktop Lenovo ThinkCentre M75q Gen 5 GM0Z35CA PC1006
8 Desktop Lenovo ThinkCentre M75q Gen 5 GM0Z35BY PC1007
9 Desktop Lenovo ThinkCentre M75q Gen 5 GM0Z35JW PC1008
10 Desktop Lenovo ThinkCentre M75q Gen 5 GM0Z35JZ PC1009
11 Desktop Lenovo ThinkCentre M75q Gen 5 GM0Z35CB PC1010
12 0 0 0 0
13 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9M NB1201
14 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9R NB1202
15 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9V NB1203
16 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9W NB1204
17 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9X NB1205
18 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4LA2 NB1206
19 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9K NB1207
20 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9P NB1208
21 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9Q NB1209
22 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9S NB1210
23 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4LA0 NB1211
24 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9G NB1212
25 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9H NB1213
26 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9N NB1214
27 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9J NB1215
28 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9F NB1216
29 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4LA1 NB1217
30 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9Y NB1218
31 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9L NB1219
32 Laptop Lenovo ThinkPad L14 Gen 6 PW0L4L9T NB1220
33 0 0 0 0
34 0 0 0 0
35 0 0 0 0
36 0 0 0 0
37 0 0 0 0
38 0 0 0 0
39 0 0 0 0
40 0 0 0 0
41 0 0 0 0
42 0 0 0 0
43 0 0 0 0
44 0 0 0 0
45 0 0 0 0
46 0 0 0 0
47 0 0 0 0
48 0 0 0 0
49 0 0 0 0
50 0 0 0 0
51 0 0 0 0
52 0 0 0 0
53 0 0 0 0
54 0 0 0 0
55 0 0 0 0
56 0 0 0 0
57 0 0 0 0
58 0 0 0 0
59 0 0 0 0
60 0 0 0 0
61 0 0 0 0
62 0 0 0 0
63 0 0 0 0
64 0 0 0 0
65 0 0 0 0
66 0 0 0 0
67 0 0 0 0
68 0 0 0 0
69 0 0 0 0
70 0 0 0 0
71 0 0 0 0
72 0 0 0 0
73 0 0 0 0
74 0 0 0 0